using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using PerfectRandom.Sulfur.Core; using PerfectRandom.Sulfur.Core.CharacterStats; using PerfectRandom.Sulfur.Core.Items; using PerfectRandom.Sulfur.Core.Stats; using PerfectRandom.Sulfur.Core.UI.ItemDescription; using PerfectRandom.Sulfur.Core.Weapons; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("PerfectOils")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PerfectOils")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("fca0f276-8db3-47a4-b077-acfc5373e719")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace PerfectOils { internal sealed class OilTraitService { internal sealed class OilDefinitionInfo { internal readonly EnchantmentDefinition Definition; internal readonly bool HasMoreBulletDrop; internal OilDefinitionInfo(EnchantmentDefinition definition) { Definition = definition; HasMoreBulletDrop = NegativeTraitPolicy.HasMoreBulletDropTrait(((Object)(object)definition == (Object)null) ? null : definition.modifiersApplied); } } private struct ModifierSignature { internal readonly ItemAttributes Attribute; internal readonly StatModType ModType; internal readonly float Value; internal readonly NegativeOilTrait Traits; internal ModifierSignature(ItemModifierContainer modifier, NegativeOilTrait traits) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) Attribute = modifier.attribute; ModType = modifier.modType; Value = modifier.value; Traits = traits; } internal bool Matches(ItemAttributes attribute, StatModifier modifier) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) return modifier != null && Attribute == attribute && ModType == modifier.Type && Math.Abs(Value - modifier.Value) <= 1E-06f; } } private readonly ManualLogSource _log; private readonly TraitConfiguration _settings; private readonly Dictionary> _classifiedBySourceId = new Dictionary>(); private readonly Dictionary _oilInfoByItem = new Dictionary(); private readonly Dictionary _oilInfoByDefinition = new Dictionary(); private readonly Dictionary _originalDurabilityFlags = new Dictionary(); private bool _initialized; private bool _loggedFirstRuntimeSuppression; private bool _warnedUnavailableDatabase; private bool _warnedEmptyDatabase; private bool _warnedNoOilDefinitions; internal bool IsInitialized => _initialized; internal OilTraitService(ManualLogSource log, TraitConfiguration settings) { _log = log; _settings = settings; } internal bool Initialize(AsyncAssetLoading assets, bool detailedLogging) { //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_0281: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Invalid comparison between Unknown and I4 if (_initialized) { return true; } if ((Object)(object)assets == (Object)null || (Object)(object)assets.itemDatabase == (Object)null || (Object)(object)assets.enchantmentDatabase == (Object)null) { if (!_warnedUnavailableDatabase) { _log.LogWarning((object)"[PerfectOils] Asset loading completed, but an oil database was unavailable; initialization will be retried."); _warnedUnavailableDatabase = true; } return false; } List rawList = assets.itemDatabase.GetRawList(); if (rawList == null) { if (!_warnedUnavailableDatabase) { _log.LogWarning((object)"[PerfectOils] Item database returned no raw item list; initialization will be retried."); _warnedUnavailableDatabase = true; } return false; } if (rawList.Count == 0) { if (!_warnedEmptyDatabase) { _log.LogWarning((object)"[PerfectOils] Item database is present but still empty; initialization will be retried."); _warnedEmptyDatabase = true; } return false; } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; int num6 = 0; int num7 = 0; int num8 = 0; int num9 = 0; for (int i = 0; i < rawList.Count; i++) { ItemDefinition val = rawList[i]; if (!IsOilItem(val)) { continue; } num++; EnchantmentDefinition val2; try { val2 = assets.enchantmentDatabase[val.appliesEnchantment]; } catch (Exception ex) { _log.LogWarning((object)("[PerfectOils] Could not resolve enchantment for oil '" + SafeOilName(val) + "': " + ex.Message)); continue; } if ((Object)(object)val2 == (Object)null) { continue; } if (!_oilInfoByDefinition.TryGetValue(val2, out var value)) { value = new OilDefinitionInfo(val2); _oilInfoByDefinition.Add(val2, value); num2++; uint key = GlobalId.op_Implicit(((EnchantmentId)(ref val2.id)).AsGlobalId()); List list = new List(); List modifiersApplied = val2.modifiersApplied; if (modifiersApplied != null) { for (int j = 0; j < modifiersApplied.Count; j++) { ItemModifierContainer val3 = modifiersApplied[j]; NegativeOilTrait negativeOilTrait = NegativeTraitPolicy.Classify(val3, value.HasMoreBulletDrop); if (negativeOilTrait == NegativeOilTrait.None) { continue; } list.Add(new ModifierSignature(val3, negativeOilTrait)); num3++; if ((negativeOilTrait & NegativeOilTrait.NegativeBulletSpeed) != NegativeOilTrait.None) { num5++; } if ((negativeOilTrait & NegativeOilTrait.NegativeDamage) != NegativeOilTrait.None) { if ((int)val3.modType == 100) { num6++; } else { num7++; } } if ((negativeOilTrait & NegativeOilTrait.NegativeBulletSize) != NegativeOilTrait.None) { num8++; } if ((negativeOilTrait & NegativeOilTrait.NegativeRpm) != NegativeOilTrait.None) { num9++; } if (_settings.ShouldRemove(negativeOilTrait)) { num4++; } if (detailedLogging) { _log.LogInfo((object)("[PerfectOils] Classified " + NegativeTraitPolicy.Describe(negativeOilTrait) + " from '" + SafeDefinitionName(val2, val) + "' [active=" + _settings.ShouldRemove(negativeOilTrait) + ", attribute=" + ((object)Unsafe.As(ref val3.attribute)/*cast due to .constrained prefix*/).ToString() + ", type=" + ((object)Unsafe.As(ref val3.modType)/*cast due to .constrained prefix*/).ToString() + ", value=" + val3.value + "].")); } } } if (list.Count > 0) { _classifiedBySourceId[key] = list; } _originalDurabilityFlags[val2] = val2.CostsDurability; } _oilInfoByItem[val] = value; } if (num == 0 || num2 == 0) { if (!_warnedNoOilDefinitions) { _log.LogWarning((object)("[PerfectOils] Scanned " + rawList.Count + " item definitions but found no runtime enchantment oils; initialization will be retried.")); _warnedNoOilDefinitions = true; } return false; } _initialized = true; _log.LogInfo((object)("[PerfectOils] Scanned " + rawList.Count + " item definitions. Indexed " + num + " oil items across " + num2 + " unique enchantments; " + num3 + " known negative modifiers were classified, and " + num4 + " are currently enabled for suppression by config. Signed-value traits: bulletSpeed=" + num5 + ", damageFlat=" + num6 + ", damagePercent=" + num7 + ", bulletSize=" + num8 + ", rpm=" + num9 + ".")); return true; } internal unsafe bool ShouldSuppress(ItemAttributes attribute, StatModifier modifier) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) if (!_initialized || modifier == null) { return false; } if (!_classifiedBySourceId.TryGetValue(modifier.SourceId, out var value)) { return false; } for (int i = 0; i < value.Count; i++) { ModifierSignature modifierSignature = value[i]; if (modifierSignature.Matches(attribute, modifier) && _settings.ShouldRemove(modifierSignature.Traits)) { if (!_loggedFirstRuntimeSuppression) { _loggedFirstRuntimeSuppression = true; ManualLogSource log = _log; string[] obj = new string[7] { "[PerfectOils] Runtime suppression is active; the first configured oil modifier was blocked [trait=", NegativeTraitPolicy.Describe(modifierSignature.Traits), ", attribute=", ((object)(*(ItemAttributes*)(&attribute))/*cast due to .constrained prefix*/).ToString(), ", sourceId=", null, null }; uint sourceId = modifier.SourceId; obj[5] = sourceId.ToString(); obj[6] = "]."; log.LogInfo((object)string.Concat(obj)); } return true; } } return false; } internal bool TryGetOilInfo(InventoryItem item, out OilDefinitionInfo info) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) info = null; if (!_initialized || (Object)(object)item == (Object)null || (Object)(object)item.itemDefinition == (Object)null) { return false; } ItemDefinition itemDefinition = item.itemDefinition; if (_oilInfoByItem.TryGetValue(itemDefinition, out info)) { return true; } if (!((EnchantmentId)(ref itemDefinition.appliesEnchantment)).IsValid) { return false; } try { EnchantmentDefinition asset = AssetAccess.GetAsset(itemDefinition.appliesEnchantment); return (Object)(object)asset != (Object)null && _oilInfoByDefinition.TryGetValue(asset, out info); } catch { info = null; return false; } } internal void RefreshDurabilityFlags(bool pluginEnabled) { bool flag = pluginEnabled && _settings.RemoveExtraDurabilityCost.Value; foreach (KeyValuePair originalDurabilityFlag in _originalDurabilityFlags) { if ((Object)(object)originalDurabilityFlag.Key != (Object)null) { originalDurabilityFlag.Key.CostsDurability = !flag && originalDurabilityFlag.Value; } } } internal void RestoreOriginalDefinitions() { foreach (KeyValuePair originalDurabilityFlag in _originalDurabilityFlags) { if ((Object)(object)originalDurabilityFlag.Key != (Object)null) { originalDurabilityFlag.Key.CostsDurability = originalDurabilityFlag.Value; } } _classifiedBySourceId.Clear(); _oilInfoByItem.Clear(); _oilInfoByDefinition.Clear(); _originalDurabilityFlags.Clear(); _initialized = false; _loggedFirstRuntimeSuppression = false; _warnedUnavailableDatabase = false; _warnedEmptyDatabase = false; _warnedNoOilDefinitions = false; } private static bool IsOilItem(ItemDefinition itemDefinition) { return (Object)(object)itemDefinition != (Object)null && itemDefinition.IsEnchantment && ((EnchantmentId)(ref itemDefinition.appliesEnchantment)).IsValid; } private static string SafeOilName(ItemDefinition oilItem) { if ((Object)(object)oilItem == (Object)null) { return ""; } if (!string.IsNullOrEmpty(oilItem.displayName)) { return oilItem.displayName; } return ((object)Unsafe.As(ref oilItem.id)/*cast due to .constrained prefix*/).ToString(); } private static string SafeDefinitionName(EnchantmentDefinition definition, ItemDefinition oilItem) { if ((Object)(object)definition != (Object)null && !string.IsNullOrEmpty(definition.enchantmentName)) { return definition.enchantmentName; } return SafeOilName(oilItem); } } [Flags] internal enum NegativeOilTrait { None = 0, DisableAiming = 1, MoreBulletDrop = 2, MoreDrag = 4, ExtraAmmoConsumeChance = 8, DecreaseAccuracyWhenMoving = 0x10, DecreaseMoveSpeed = 0x20, DecreaseJumpPower = 0x40, DecreaseLootChanceMultiplier = 0x80, DisableMoneyDrops = 0x100, DisableOrganDrops = 0x200, NegativeBulletSpeed = 0x400, NegativeDamage = 0x800, NegativeBulletSize = 0x1000, NegativeRpm = 0x2000, ExtraDurabilityCost = 0x4000 } internal static class NegativeTraitPolicy { private const float Epsilon = 1E-06f; internal static bool HasMoreBulletDropTrait(IList modifiers) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if (modifiers == null) { return false; } for (int i = 0; i < modifiers.Count; i++) { ItemModifierContainer val = modifiers[i]; if (val != null && (int)val.attribute == 102 && IncreasesAttribute(val)) { return true; } } return false; } internal static NegativeOilTrait Classify(ItemModifierContainer modifier, bool oilHasMoreBulletDrop) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Invalid comparison between Unknown and I4 //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected I4, but got Unknown //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected I4, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Invalid comparison between Unknown and I4 //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Invalid comparison between Unknown and I4 if (modifier == null) { return NegativeOilTrait.None; } ItemAttributes attribute = modifier.attribute; ItemAttributes val = attribute; if ((int)val <= 52) { switch (val - 1) { default: if ((int)val != 41) { if ((int)val != 52) { break; } return DecreasesAttribute(modifier) ? NegativeOilTrait.NegativeBulletSize : NegativeOilTrait.None; } return (oilHasMoreBulletDrop && IncreasesAttribute(modifier)) ? NegativeOilTrait.MoreBulletDrop : NegativeOilTrait.None; case 7: return IncreasesAttribute(modifier) ? NegativeOilTrait.DisableAiming : NegativeOilTrait.None; case 2: return IncreasesAttribute(modifier) ? NegativeOilTrait.ExtraAmmoConsumeChance : NegativeOilTrait.None; case 0: return DecreasesAttribute(modifier) ? NegativeOilTrait.DecreaseAccuracyWhenMoving : NegativeOilTrait.None; case 16: return DecreasesAttribute(modifier) ? NegativeOilTrait.DecreaseMoveSpeed : NegativeOilTrait.None; case 14: return DecreasesAttribute(modifier) ? NegativeOilTrait.DecreaseJumpPower : NegativeOilTrait.None; case 15: return DecreasesAttribute(modifier) ? NegativeOilTrait.DecreaseLootChanceMultiplier : NegativeOilTrait.None; case 8: return IncreasesAttribute(modifier) ? NegativeOilTrait.DisableMoneyDrops : NegativeOilTrait.None; case 9: return IncreasesAttribute(modifier) ? NegativeOilTrait.DisableOrganDrops : NegativeOilTrait.None; case 5: case 6: return DecreasesAttribute(modifier) ? NegativeOilTrait.NegativeDamage : NegativeOilTrait.None; case 1: case 3: case 4: case 10: case 11: case 12: case 13: break; } } else { if ((int)val == 56) { return DecreasesAttribute(modifier) ? NegativeOilTrait.NegativeRpm : NegativeOilTrait.None; } switch (val - 61) { default: if ((int)val != 102) { break; } return IncreasesAttribute(modifier) ? NegativeOilTrait.MoreBulletDrop : NegativeOilTrait.None; case 4: return IncreasesAttribute(modifier) ? NegativeOilTrait.MoreDrag : NegativeOilTrait.None; case 0: if (!DecreasesAttribute(modifier)) { return NegativeOilTrait.None; } return (NegativeOilTrait)(0x400 | (oilHasMoreBulletDrop ? 2 : 0)); case 2: return IncreasesAttribute(modifier) ? NegativeOilTrait.ExtraDurabilityCost : NegativeOilTrait.None; case 1: case 3: break; } } return NegativeOilTrait.None; } internal static string Describe(NegativeOilTrait traits) { if (traits == NegativeOilTrait.None) { return "None"; } List list = new List(); AddName(list, traits, NegativeOilTrait.DisableAiming, "Disable Aiming"); AddName(list, traits, NegativeOilTrait.MoreBulletDrop, "More Bullet Drop"); AddName(list, traits, NegativeOilTrait.MoreDrag, "More Drag"); AddName(list, traits, NegativeOilTrait.ExtraAmmoConsumeChance, "Extra Ammo Consume Chance"); AddName(list, traits, NegativeOilTrait.DecreaseAccuracyWhenMoving, "Decrease Accuracy When Moving"); AddName(list, traits, NegativeOilTrait.DecreaseMoveSpeed, "Decrease Move Speed"); AddName(list, traits, NegativeOilTrait.DecreaseJumpPower, "Decrease Jump Power"); AddName(list, traits, NegativeOilTrait.DecreaseLootChanceMultiplier, "Decrease Loot Chance Multiplier"); AddName(list, traits, NegativeOilTrait.DisableMoneyDrops, "Disable Money Drops"); AddName(list, traits, NegativeOilTrait.DisableOrganDrops, "Disable Organ Drops"); AddName(list, traits, NegativeOilTrait.NegativeBulletSpeed, "Negative Bullet Speed"); AddName(list, traits, NegativeOilTrait.NegativeDamage, "Negative Damage"); AddName(list, traits, NegativeOilTrait.NegativeBulletSize, "Negative Bullet Size"); AddName(list, traits, NegativeOilTrait.NegativeRpm, "Negative RPM"); AddName(list, traits, NegativeOilTrait.ExtraDurabilityCost, "Extra Oil Durability Cost"); return string.Join(" + ", list.ToArray()); } private static bool IncreasesAttribute(ItemModifierContainer modifier) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return IsSupportedModType(modifier.modType) && modifier.value > 1E-06f; } private static bool DecreasesAttribute(ItemModifierContainer modifier) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return IsSupportedModType(modifier.modType) && modifier.value < -1E-06f; } private static bool IsSupportedModType(StatModType modType) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Invalid comparison between Unknown and I4 //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Invalid comparison between Unknown and I4 return (int)modType == 100 || (int)modType == 200 || (int)modType == 300; } private static void AddName(List names, NegativeOilTrait traits, NegativeOilTrait flag, string name) { if ((traits & flag) != NegativeOilTrait.None) { names.Add(name); } } } [BepInPlugin("com.ryuka.sulfur.perfectoils", "Perfect Oils", "1.3.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.ryuka.sulfur.perfectoils"; public const string PluginName = "Perfect Oils"; public const string PluginVersion = "1.3.0"; private const float InitializationRetryInterval = 1f; private Harmony _harmony; private AsyncAssetLoading _assetLoader; private float _nextInitializationAttempt; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal ConfigEntry Enabled { get; private set; } internal ConfigEntry ShowRemovedTraitsWithStrikethrough { get; private set; } internal ConfigEntry DetailedLogging { get; private set; } internal TraitConfiguration TraitSettings { get; private set; } internal OilTraitService OilTraits { get; private set; } internal TooltipStrikeRenderer TooltipRenderer { get; private set; } private void Awake() { //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Enable Perfect Oils. Individual undesirable traits can be selected in the Traits section."); ShowRemovedTraitsWithStrikethrough = ((BaseUnityPlugin)this).Config.Bind("Display", "ShowRemovedTraitsWithStrikethrough", true, "Keep the original undesirable-trait text in oil tooltips and draw a strikethrough over lines currently disabled by this mod."); DetailedLogging = ((BaseUnityPlugin)this).Config.Bind("Debug", "DetailedLogging", false, "Log each classified oil modifier and tooltip line. Keep disabled during normal play."); TraitSettings = new TraitConfiguration(((BaseUnityPlugin)this).Config); OilTraits = new OilTraitService(((BaseUnityPlugin)this).Logger, TraitSettings); TooltipRenderer = new TooltipStrikeRenderer(OilTraits, TraitSettings, ((BaseUnityPlugin)this).Logger); Enabled.SettingChanged += OnDurabilitySettingChanged; TraitSettings.RemoveExtraDurabilityCost.SettingChanged += OnDurabilitySettingChanged; _harmony = new Harmony("com.ryuka.sulfur.perfectoils"); _harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[PerfectOils] Patches applied. Waiting for the SULFUR asset databases."); } private void Update() { if (OilTraits != null && !OilTraits.IsInitialized && !(Time.unscaledTime < _nextInitializationAttempt)) { if ((Object)(object)_assetLoader == (Object)null) { _assetLoader = Object.FindFirstObjectByType(); } if ((Object)(object)_assetLoader == (Object)null || !_assetLoader.loadingDone) { _nextInitializationAttempt = Time.unscaledTime + 1f; } else { TryInitializeOilTraits(_assetLoader); } } } internal void NotifyAssetsReady(AsyncAssetLoading assetLoader) { if (!((Object)(object)assetLoader == (Object)null)) { _assetLoader = assetLoader; TryInitializeOilTraits(assetLoader); } } private void TryInitializeOilTraits(AsyncAssetLoading assetLoader) { if (OilTraits != null && !OilTraits.IsInitialized) { if (!OilTraits.Initialize(assetLoader, DetailedLogging.Value)) { _nextInitializationAttempt = Time.unscaledTime + 1f; } else { OilTraits.RefreshDurabilityFlags(Enabled.Value); } } } private void OnDurabilitySettingChanged(object sender, EventArgs eventArgs) { if (OilTraits != null && OilTraits.IsInitialized) { OilTraits.RefreshDurabilityFlags(Enabled.Value); } } private void OnDestroy() { Enabled.SettingChanged -= OnDurabilitySettingChanged; if (TraitSettings != null) { TraitSettings.RemoveExtraDurabilityCost.SettingChanged -= OnDurabilitySettingChanged; } if (OilTraits != null) { OilTraits.RestoreOriginalDefinitions(); } if (_harmony != null) { _harmony.UnpatchSelf(); } if (Instance == this) { Instance = null; Log = null; } } } internal sealed class TooltipStrikeRenderer { private enum LineKind { Attribute, Description } private sealed class LineSpec { internal readonly LineKind Kind; internal readonly string Key; internal readonly string Value; internal readonly bool IsRemoved; internal LineSpec(LineKind kind, string key, string value, bool isRemoved) { Kind = kind; Key = key ?? string.Empty; Value = value ?? string.Empty; IsRemoved = isRemoved; } } private sealed class RenderedLine { internal readonly LineKind Kind; internal readonly int SiblingIndex; internal readonly string Key; internal readonly string Value; internal readonly TMP_Text[] TextComponents; internal RenderedLine(LineKind kind, int siblingIndex, string key, string value, TMP_Text[] textComponents) { Kind = kind; SiblingIndex = siblingIndex; Key = key ?? string.Empty; Value = value ?? string.Empty; TextComponents = textComponents; } } private static class LocalizationBridge { private static readonly MethodInfo TryGetTranslationMethod = FindTryGetTranslation(); internal static string GetTranslationOrFallback(string term, string fallback) { if (TryGetTranslationMethod != null) { try { object[] array = new object[9] { term, null, true, 0, true, false, null, null, true }; bool flag = (bool)TryGetTranslationMethod.Invoke(null, array); string text = array[1] as string; if (flag && !string.IsNullOrEmpty(text)) { return text; } } catch { } } return fallback ?? string.Empty; } private static MethodInfo FindTryGetTranslation() { Type type = AccessTools.TypeByName("I2.Loc.LocalizationManager"); if (type == null) { return null; } MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (!(methodInfo.Name != "TryGetTranslation")) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 9 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(string).MakeByRefType()) { return methodInfo; } } } return null; } } private static readonly FieldInfo AttributesInUseField = AccessTools.Field(typeof(ItemDescription), "attributesInUse"); private static readonly FieldInfo DescriptionTextInUseField = AccessTools.Field(typeof(ItemDescription), "descriptionTextInUse"); private static readonly FieldInfo KeyTextField = AccessTools.Field(typeof(ItemDescriptionAttribute), "keyText"); private static readonly FieldInfo ValueTextField = AccessTools.Field(typeof(ItemDescriptionAttribute), "valueText"); private static readonly FieldInfo ModifierTextField = AccessTools.Field(typeof(ItemDescriptionAttribute), "modifierText"); private static readonly MethodInfo FormatModifierValueMethod = AccessTools.Method(typeof(ItemDescription), "GetValueAsStringDependingOnModType", new Type[2] { typeof(ItemModifierContainer), typeof(ItemAttribute) }, (Type[])null); private readonly OilTraitService _oilTraits; private readonly TraitConfiguration _settings; private readonly ManualLogSource _log; private bool _loggedFirstStrikethrough; internal TooltipStrikeRenderer(OilTraitService oilTraits, TraitConfiguration settings, ManualLogSource log) { _oilTraits = oilTraits; _settings = settings; _log = log; } internal void Apply(ItemDescription description, InventoryItem item, bool detailedLogging) { if ((Object)(object)description == (Object)null || (Object)(object)item == (Object)null || !_oilTraits.TryGetOilInfo(item, out var info) || info == null || (Object)(object)info.Definition == (Object)null || info.Definition.modifiersApplied == null) { return; } List list = BuildLineSpecs(description, info); if (list.Count == 0) { return; } List list2 = CollectRenderedLines(description); if (list2.Count == 0) { return; } List list3 = FindBestOrderedMatch(list2, list); if (list3 == null) { list3 = FindRemovedLinesFallback(list2, list); if (detailedLogging) { _log.LogWarning((object)("[PerfectOils] Could not match the complete oil modifier block for '" + SafeItemName(item) + "'; used exact removed-line fallback matching.")); } } if (list3 == null) { return; } int num = 0; for (int i = 0; i < list.Count && i < list3.Count; i++) { if (list[i].IsRemoved && list3[i] >= 0) { ApplyStrikethrough(list2[list3[i]]); num++; } } if (num > 0 && !_loggedFirstStrikethrough) { _loggedFirstStrikethrough = true; _log.LogInfo((object)"[PerfectOils] Tooltip integration is active; removed oil traits are being shown with strikethrough."); } if (detailedLogging && num > 0) { _log.LogInfo((object)("[PerfectOils] Marked " + num + " removed trait line(s) in the tooltip for '" + SafeItemName(item) + "'.")); } } private List BuildLineSpecs(ItemDescription description, OilTraitService.OilDefinitionInfo info) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) List list = new List(); List modifiersApplied = info.Definition.modifiersApplied; for (int i = 0; i < modifiersApplied.Count; i++) { ItemModifierContainer val = modifiersApplied[i]; if (val == null || (int)val.attribute == 0) { continue; } ItemAttribute asset; try { asset = AssetAccess.GetAsset(val.attribute); } catch { continue; } if ((Object)(object)asset == (Object)null || !asset.showInItemDescription) { continue; } NegativeOilTrait traits = NegativeTraitPolicy.Classify(val, info.HasMoreBulletDrop); bool isRemoved = _settings.ShouldRemove(traits); if (asset.simplifiedModAmount) { bool flag = val.value > 0f; string term = string.Format(flag ? "ItemAttributes/{0}_simplifiedIncrease" : "ItemAttributes/{0}_simplifiedDecrease", val.attribute); string fallback = (flag ? asset.simplifiedIncreaseString : asset.simplifiedDecreaseString); string translationOrFallback = LocalizationBridge.GetTranslationOrFallback(term, fallback); if (!string.IsNullOrEmpty(translationOrFallback)) { list.Add(new LineSpec(LineKind.Description, translationOrFallback, string.Empty, isRemoved)); } continue; } string term2 = $"ItemAttributes/{val.attribute}_itemDescription"; string translationOrFallback2 = LocalizationBridge.GetTranslationOrFallback(term2, asset.itemDescriptionName); if (asset.isBooleanAttribute) { if (!string.IsNullOrEmpty(translationOrFallback2)) { list.Add(new LineSpec(LineKind.Description, translationOrFallback2, string.Empty, isRemoved)); } } else { string value = FormatModifierValue(description, val, asset); string key = translationOrFallback2 + (string.IsNullOrEmpty(value) ? string.Empty : ":"); list.Add(new LineSpec(LineKind.Attribute, key, value, isRemoved)); } } return list; } private static string FormatModifierValue(ItemDescription description, ItemModifierContainer modifier, ItemAttribute attribute) { if (FormatModifierValueMethod == null) { return modifier.GetValueString(); } try { return ((string)FormatModifierValueMethod.Invoke(description, new object[2] { modifier, attribute })) ?? string.Empty; } catch { return modifier.GetValueString() ?? string.Empty; } } private static List CollectRenderedLines(ItemDescription description) { List list = new List(); List list2 = ((AttributesInUseField == null) ? null : (AttributesInUseField.GetValue(description) as List)); if (list2 != null) { for (int i = 0; i < list2.Count; i++) { ItemDescriptionAttribute val = list2[i]; if (!((Object)(object)val == (Object)null)) { TMP_Text textField = GetTextField(KeyTextField, val); TMP_Text textField2 = GetTextField(ValueTextField, val); TMP_Text textField3 = GetTextField(ModifierTextField, val); list.Add(new RenderedLine(LineKind.Attribute, ((Component)val).transform.GetSiblingIndex(), GetComparableText(textField), GetComparableText(textField2), (TMP_Text[])(object)new TMP_Text[3] { textField, textField2, textField3 })); } } } List list3 = ((DescriptionTextInUseField == null) ? null : (DescriptionTextInUseField.GetValue(description) as List)); if (list3 != null) { for (int j = 0; j < list3.Count; j++) { ItemDescriptionText val2 = list3[j]; if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.textComp == (Object)null)) { int siblingIndex = ((Component)val2).transform.GetSiblingIndex(); string comparableText = GetComparableText((TMP_Text)(object)val2.textComp); string empty = string.Empty; TMP_Text[] textComponents = (TMP_Text[])(object)new TextMeshProUGUI[1] { val2.textComp }; list.Add(new RenderedLine(LineKind.Description, siblingIndex, comparableText, empty, textComponents)); } } } list.Sort(delegate(RenderedLine left, RenderedLine right) { int siblingIndex2 = left.SiblingIndex; return siblingIndex2.CompareTo(right.SiblingIndex); }); return list; } private static TMP_Text GetTextField(FieldInfo field, object instance) { return (TMP_Text)((field == null) ? null : /*isinst with value type is only supported in some contexts*/); } private static string GetComparableText(TMP_Text text) { return ((Object)(object)text == (Object)null) ? string.Empty : RemoveOwnStrikeTags(text.text ?? string.Empty); } private static List FindBestOrderedMatch(List lines, List specs) { List result = null; int num = int.MaxValue; for (int i = 0; i < lines.Count; i++) { if (!Matches(lines[i], specs[0])) { continue; } List list = new List(specs.Count) { i }; int num2 = i + 1; bool flag = true; for (int j = 1; j < specs.Count; j++) { int num3 = -1; for (int k = num2; k < lines.Count; k++) { if (Matches(lines[k], specs[j])) { num3 = k; break; } } if (num3 < 0) { flag = false; break; } list.Add(num3); num2 = num3 + 1; } if (flag) { int num4 = list[list.Count - 1] - list[0]; if (num4 < num) { result = list; num = num4; } } } return result; } private static List FindRemovedLinesFallback(List lines, List specs) { List list = new List(specs.Count); HashSet hashSet = new HashSet(); bool flag = false; for (int i = 0; i < specs.Count; i++) { if (!specs[i].IsRemoved) { list.Add(-1); continue; } int item = -1; for (int j = 0; j < lines.Count; j++) { if (!hashSet.Contains(j) && Matches(lines[j], specs[i])) { item = j; hashSet.Add(j); flag = true; break; } } list.Add(item); } return flag ? list : null; } private static bool Matches(RenderedLine line, LineSpec spec) { return line.Kind == spec.Kind && string.Equals(line.Key, spec.Key, StringComparison.Ordinal) && string.Equals(line.Value, spec.Value, StringComparison.Ordinal); } private static void ApplyStrikethrough(RenderedLine line) { for (int i = 0; i < line.TextComponents.Length; i++) { TMP_Text val = line.TextComponents[i]; if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.text)) { string text = RemoveOwnStrikeTags(val.text); val.text = "" + text + ""; } } } private static string RemoveOwnStrikeTags(string text) { if (string.IsNullOrEmpty(text)) { return string.Empty; } if (text.StartsWith("", StringComparison.Ordinal) && text.EndsWith("", StringComparison.Ordinal)) { return text.Substring("".Length, text.Length - "".Length - "".Length); } return text; } private static string SafeItemName(InventoryItem item) { if ((Object)(object)item == (Object)null || (Object)(object)item.itemDefinition == (Object)null) { return ""; } return string.IsNullOrEmpty(item.itemDefinition.displayName) ? ((object)item.itemDefinition).ToString() : item.itemDefinition.displayName; } } internal sealed class TraitConfiguration { internal ConfigEntry RemoveDisableAiming { get; private set; } internal ConfigEntry RemoveMoreBulletDrop { get; private set; } internal ConfigEntry RemoveMoreDrag { get; private set; } internal ConfigEntry RemoveExtraAmmoConsumeChance { get; private set; } internal ConfigEntry RemoveDecreaseAccuracyWhenMoving { get; private set; } internal ConfigEntry RemoveDecreaseMoveSpeed { get; private set; } internal ConfigEntry RemoveDecreaseJumpPower { get; private set; } internal ConfigEntry RemoveDecreaseLootChanceMultiplier { get; private set; } internal ConfigEntry RemoveDisableMoneyDrops { get; private set; } internal ConfigEntry RemoveDisableOrganDrops { get; private set; } internal ConfigEntry RemoveNegativeBulletSpeed { get; private set; } internal ConfigEntry RemoveNegativeDamage { get; private set; } internal ConfigEntry RemoveNegativeBulletSize { get; private set; } internal ConfigEntry RemoveNegativeRpm { get; private set; } internal ConfigEntry RemoveExtraDurabilityCost { get; private set; } internal TraitConfiguration(ConfigFile config) { RemoveDisableAiming = config.Bind("Traits", "RemoveDisableAiming", true, "Remove oil modifiers that disable aiming down sights."); RemoveMoreBulletDrop = config.Bind("Traits", "RemoveMoreBulletDrop", false, "Remove increased projectile gravity and its same-oil speed/mass companion penalties."); RemoveMoreDrag = config.Bind("Traits", "RemoveMoreDrag", false, "Remove oil modifiers that increase projectile drag."); RemoveExtraAmmoConsumeChance = config.Bind("Traits", "RemoveExtraAmmoConsumeChance", true, "Remove oil modifiers that add a chance to consume extra ammunition."); RemoveDecreaseAccuracyWhenMoving = config.Bind("Traits", "RemoveDecreaseAccuracyWhenMoving", true, "Remove oil modifiers that reduce accuracy while moving."); RemoveDecreaseMoveSpeed = config.Bind("Traits", "RemoveDecreaseMoveSpeed", true, "Remove oil modifiers that reduce movement speed while holding the weapon."); RemoveDecreaseJumpPower = config.Bind("Traits", "RemoveDecreaseJumpPower", true, "Remove oil modifiers that reduce jump power while holding the weapon."); RemoveDecreaseLootChanceMultiplier = config.Bind("Traits", "RemoveDecreaseLootChanceMultiplier", true, "Remove oil modifiers that reduce the loot chance multiplier."); RemoveDisableMoneyDrops = config.Bind("Traits", "RemoveDisableMoneyDrops", true, "Remove oil modifiers that disable Sulf/money drops."); RemoveDisableOrganDrops = config.Bind("Traits", "RemoveDisableOrganDrops", true, "Remove oil modifiers that disable organ drops."); RemoveNegativeBulletSpeed = config.Bind("Traits", "RemoveNegativeBulletSpeed", false, "Remove ProjectileTimeScale modifiers only when they reduce bullet speed. Positive bullet-speed modifiers remain active."); RemoveNegativeDamage = config.Bind("Traits", "RemoveNegativeDamage", false, "Remove flat or percentage damage modifiers only when their signed value is negative. Positive damage modifiers remain active."); RemoveNegativeBulletSize = config.Bind("Traits", "RemoveNegativeBulletSize", false, "Remove ProjectileScale modifiers only when they reduce bullet size. Positive bullet-size modifiers remain active."); RemoveNegativeRpm = config.Bind("Traits", "RemoveNegativeRpm", false, "Remove RPM modifiers only when their signed value is negative. Positive fire-rate modifiers remain active."); RemoveExtraDurabilityCost = config.Bind("General", "RemoveExtraDurabilityCost", false, "Remove oil-specific extra durability consumption. This setting is independent from the other undesirable traits."); } internal bool ShouldRemove(NegativeOilTrait traits) { return IsEnabled(traits, NegativeOilTrait.DisableAiming, RemoveDisableAiming) || IsEnabled(traits, NegativeOilTrait.MoreBulletDrop, RemoveMoreBulletDrop) || IsEnabled(traits, NegativeOilTrait.MoreDrag, RemoveMoreDrag) || IsEnabled(traits, NegativeOilTrait.ExtraAmmoConsumeChance, RemoveExtraAmmoConsumeChance) || IsEnabled(traits, NegativeOilTrait.DecreaseAccuracyWhenMoving, RemoveDecreaseAccuracyWhenMoving) || IsEnabled(traits, NegativeOilTrait.DecreaseMoveSpeed, RemoveDecreaseMoveSpeed) || IsEnabled(traits, NegativeOilTrait.DecreaseJumpPower, RemoveDecreaseJumpPower) || IsEnabled(traits, NegativeOilTrait.DecreaseLootChanceMultiplier, RemoveDecreaseLootChanceMultiplier) || IsEnabled(traits, NegativeOilTrait.DisableMoneyDrops, RemoveDisableMoneyDrops) || IsEnabled(traits, NegativeOilTrait.DisableOrganDrops, RemoveDisableOrganDrops) || IsEnabled(traits, NegativeOilTrait.NegativeBulletSpeed, RemoveNegativeBulletSpeed) || IsEnabled(traits, NegativeOilTrait.NegativeDamage, RemoveNegativeDamage) || IsEnabled(traits, NegativeOilTrait.NegativeBulletSize, RemoveNegativeBulletSize) || IsEnabled(traits, NegativeOilTrait.NegativeRpm, RemoveNegativeRpm) || IsEnabled(traits, NegativeOilTrait.ExtraDurabilityCost, RemoveExtraDurabilityCost); } private static bool IsEnabled(NegativeOilTrait traits, NegativeOilTrait flag, ConfigEntry setting) { return (traits & flag) != NegativeOilTrait.None && setting.Value; } } } namespace PerfectOils.Patches { [HarmonyPatch(typeof(AsyncAssetLoading), "set_loadingDone")] internal static class AssetLoadingPatches { [HarmonyPostfix] private static void AfterLoadingDoneSet(AsyncAssetLoading __instance, bool value) { Plugin instance = Plugin.Instance; if (value && !((Object)(object)instance == (Object)null)) { instance.NotifyAssetsReady(__instance); } } } [HarmonyPatch] internal static class ItemDescriptionPatches { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(ItemDescription), "Setup", new Type[1] { typeof(InventoryItem) }, (Type[])null); } [HarmonyPostfix] private static void AfterSetup(ItemDescription __instance, InventoryItem __0) { Plugin instance = Plugin.Instance; if (!((Object)(object)instance == (Object)null) && instance.Enabled.Value && instance.ShowRemovedTraitsWithStrikethrough.Value && instance.TooltipRenderer != null) { instance.TooltipRenderer.Apply(__instance, __0, instance.DetailedLogging.Value); } } } [HarmonyPatch] internal static class ItemStatsPatches { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(ItemStats), "AddModifier", new Type[2] { typeof(ItemAttributes), typeof(StatModifier) }, (Type[])null); } [HarmonyPrefix] private static bool BeforeAddModifier(ItemAttributes __0, StatModifier __1) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null || !instance.Enabled.Value || instance.OilTraits == null) { return true; } return !instance.OilTraits.ShouldSuppress(__0, __1); } } [HarmonyPatch(typeof(Weapon), "SyncEnchantments")] internal static class WeaponPatches { [HarmonyPrefix] private static void ResetCachedAimingState(ref bool ___aimingDisabled) { ___aimingDisabled = false; } } }