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 EvenBetterExponentialItems v1.7.0
plugins\EvenBetterExponentialItems\EvenBetterExponentialItems.dll
Decompiled 3 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Microsoft.CodeAnalysis; using On.RoR2; using R2API; using RoR2; using UnityEngine; using UnityEngine.Networking; [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("EvenBetterExponentialItems")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.7.0.0")] [assembly: AssemblyInformationalVersion("1.7.0")] [assembly: AssemblyProduct("Even Better Exponential Items")] [assembly: AssemblyTitle("EvenBetterExponentialItems")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.7.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 EvenBetterExponentialItems { public enum AdaptiveScalingCurve { TrackDifficulty, FrontLoaded, BackLoaded, Flat } internal sealed class PlayerAdaptiveState { public float ComebackLowHpSeconds; } internal readonly struct AdaptiveMultipliers { public readonly double Growth; public readonly double Cap; public static AdaptiveMultipliers Identity => new AdaptiveMultipliers(1.0, 1.0); public AdaptiveMultipliers(double growth, double cap) { Growth = growth; Cap = cap; } } internal sealed class AdaptiveScalingManager { private readonly ExponentialItemsConfig _config; private readonly ManualLogSource _log; private readonly Func<bool> _isExponentsArtifactEnabled; private readonly Dictionary<int, PlayerAdaptiveState> _playerState = new Dictionary<int, PlayerAdaptiveState>(); private readonly Dictionary<int, AdaptiveMultipliers> _tickCache = new Dictionary<int, AdaptiveMultipliers>(); private int _cacheFrame = -1; private float _teleporterBurstEndTime; private int _lastObservedStageClearCount = -1; public AdaptiveScalingManager(ExponentialItemsConfig config, ManualLogSource log, Func<bool> isExponentsArtifactEnabled) { _config = config; _log = log; _isExponentsArtifactEnabled = isExponentsArtifactEnabled ?? ((Func<bool>)(() => false)); } public bool IsLayerActive() { try { if (!_config.AdaptiveScalingEnabled.Value) { return false; } if (_config.AdaptiveRequireArtifact.Value && !_isExponentsArtifactEnabled()) { return false; } return true; } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] IsLayerActive failed: " + ex)); return false; } } public double GetGrowthMultiplier(CharacterBody body) { return GetMultipliers(body).Growth; } public double GetCapMultiplier(CharacterBody body) { return GetMultipliers(body).Cap; } public AdaptiveMultipliers GetMultipliers(CharacterBody body) { if (!IsLayerActive()) { return AdaptiveMultipliers.Identity; } try { int key = ResolvePlayerKey(body); int frameCount = Time.frameCount; if (_cacheFrame != frameCount) { _tickCache.Clear(); _cacheFrame = frameCount; } if (_tickCache.TryGetValue(key, out var value)) { return value; } AdaptiveMultipliers adaptiveMultipliers = ComputeMultipliers(body); _tickCache[key] = adaptiveMultipliers; return adaptiveMultipliers; } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] GetMultipliers failed: " + ex)); return AdaptiveMultipliers.Identity; } } public void OnRunStart() { try { _playerState.Clear(); _tickCache.Clear(); _cacheFrame = -1; _teleporterBurstEndTime = 0f; _lastObservedStageClearCount = -1; } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] OnRunStart failed: " + ex)); } } public void OnStageStart() { try { _tickCache.Clear(); _cacheFrame = -1; foreach (PlayerAdaptiveState value in _playerState.Values) { value.ComebackLowHpSeconds = 0f; } } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] OnStageStart failed: " + ex)); } } public void OnTeleporterFinished() { try { if (IsLayerActive() && _config.AdaptiveTeleporterBurst.Value) { float num = Mathf.Max(0f, _config.AdaptiveTeleporterBurstSeconds.Value); _teleporterBurstEndTime = Mathf.Max(_teleporterBurstEndTime, Time.time + num); } } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] OnTeleporterFinished failed: " + ex)); } } private void TryDetectStageClearBurst() { Run instance = Run.instance; if ((Object)(object)instance == (Object)null) { _lastObservedStageClearCount = -1; return; } int stageClearCount = instance.stageClearCount; if (_lastObservedStageClearCount >= 0 && stageClearCount > _lastObservedStageClearCount) { OnTeleporterFinished(); } _lastObservedStageClearCount = stageClearCount; } public void OnFixedUpdate() { try { TryDetectStageClearBurst(); } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] TryDetectStageClearBurst failed: " + ex)); } if (!IsLayerActive() || !_config.AdaptiveComebackScaling.Value) { return; } try { float num = Mathf.Clamp01(_config.AdaptiveComebackHpFraction.Value); float num2 = Mathf.Max(0.1f, _config.AdaptiveComebackSeconds.Value); float fixedDeltaTime = Time.fixedDeltaTime; for (int i = 0; i < CharacterBody.readOnlyInstancesList.Count; i++) { CharacterBody val = CharacterBody.readOnlyInstancesList[i]; if ((Object)(object)val == (Object)null || !val.isPlayerControlled) { continue; } int key = ResolvePlayerKey(val); if (!_playerState.TryGetValue(key, out var value)) { value = new PlayerAdaptiveState(); _playerState[key] = value; } HealthComponent healthComponent = val.healthComponent; if ((Object)(object)healthComponent == (Object)null || healthComponent.fullCombinedHealth <= 0f) { value.ComebackLowHpSeconds = 0f; continue; } if (healthComponent.combinedHealth / healthComponent.fullCombinedHealth <= num) { value.ComebackLowHpSeconds += fixedDeltaTime; } else { value.ComebackLowHpSeconds = 0f; } value.ComebackLowHpSeconds = Mathf.Min(value.ComebackLowHpSeconds, num2 * 4f); } } catch (Exception ex2) { _log.LogError((object)("[AdaptiveScaling] OnFixedUpdate failed: " + ex2)); } } public string TryFormatTooltipSuffix(CharacterBody body) { if (!IsLayerActive()) { return string.Empty; } try { AdaptiveMultipliers multipliers = GetMultipliers(body); if (Math.Abs(multipliers.Growth - 1.0) < 0.005 && Math.Abs(multipliers.Cap - 1.0) < 0.005) { return string.Empty; } Run instance = Run.instance; int num = (((Object)(object)instance != (Object)null) ? instance.stageClearCount : 0); return $"\n<color=#7FD4A8>Adaptive: {multipliers.Growth:F2}x growth this stage (stage {num})</color>"; } catch (Exception ex) { _log.LogError((object)("[AdaptiveScaling] TryFormatTooltipSuffix failed: " + ex)); return string.Empty; } } private AdaptiveMultipliers ComputeMultipliers(CharacterBody body) { double num = Math.Max(0.01, _config.AdaptiveGrowthMin.Value); double num2 = Math.Max(num, _config.AdaptiveGrowthMax.Value); double num3 = Math.Max(0.01, _config.AdaptiveCapMin.Value); double max = Math.Max(num3, _config.AdaptiveCapMax.Value); Run instance = Run.instance; float difficulty = (((Object)(object)instance != (Object)null) ? instance.difficultyCoefficient : 1f); int stageClear = (((Object)(object)instance != (Object)null) ? instance.stageClearCount : 0); double growth = EvaluateGrowthCurve(_config.AdaptiveCurveShape.Value, difficulty, stageClear, num, num2); double value = EvaluateTrackDifficultyCap(difficulty, num3, max); if (_config.AdaptiveTeleporterBurst.Value && Time.time < _teleporterBurstEndTime) { growth = num2; } growth = ApplyComebackBonus(body, growth, num2); growth = ApplyMultiplayerEqualization(body, growth, num, num2); growth = Clamp(growth, num, num2); value = Clamp(value, num3, max); return new AdaptiveMultipliers(growth, value); } private double ApplyComebackBonus(CharacterBody body, double growth, double growthMax) { if (!_config.AdaptiveComebackScaling.Value || (Object)(object)body == (Object)null) { return growth; } int key = ResolvePlayerKey(body); if (!_playerState.TryGetValue(key, out var value)) { return growth; } float num = Mathf.Max(0.1f, _config.AdaptiveComebackSeconds.Value); if (value.ComebackLowHpSeconds < num) { return growth; } double num2 = Math.Max(0.0, _config.AdaptiveComebackMaxBonus.Value); double num3 = Math.Min(1.0, (value.ComebackLowHpSeconds - num) / num); return growth * (1.0 + num2 * num3); } private double ApplyMultiplayerEqualization(CharacterBody body, double growth, double growthMin, double growthMax) { if (!_config.AdaptiveMultiplayerEqualization.Value || (Object)(object)body == (Object)null) { return growth; } if (!TryGetGroupItemStats(out var playerCount, out var average, out var localCount)) { return growth; } if (playerCount <= 1) { return growth; } double num = Math.Max(0.0, _config.AdaptiveEqualizationStrength.Value); float num2 = average - (float)localCount; double num3 = 1.0 + num * (double)num2; return Clamp(growth * num3, growthMin, growthMax); } private static bool TryGetGroupItemStats(out int playerCount, out float average, out int localCount) { playerCount = 0; average = 0f; localCount = 0; int num = 0; for (int i = 0; i < CharacterBody.readOnlyInstancesList.Count; i++) { CharacterBody val = CharacterBody.readOnlyInstancesList[i]; if (!((Object)(object)val == (Object)null) && val.isPlayerControlled) { int num2 = CountTotalItemStacks(val.inventory); num += num2; playerCount++; if (((NetworkBehaviour)val).isLocalPlayer) { localCount = num2; } } } if (playerCount <= 0) { return false; } average = (float)num / (float)playerCount; return true; } private static int CountTotalItemStacks(Inventory inventory) { if ((Object)(object)inventory == (Object)null) { return 0; } int num = 0; try { int itemCount = ItemCatalog.itemCount; for (int i = 0; i < itemCount; i++) { num += inventory.GetItemCountPermanent((ItemIndex)i); } } catch { } return num; } private static double EvaluateGrowthCurve(AdaptiveScalingCurve curve, float difficulty, int stageClear, double min, double max) { return curve switch { AdaptiveScalingCurve.FrontLoaded => EvaluateFrontLoaded(stageClear, min, max), AdaptiveScalingCurve.BackLoaded => EvaluateBackLoaded(stageClear, min, max), AdaptiveScalingCurve.Flat => 1.0, _ => EvaluateTrackDifficultyGrowth(difficulty, min, max), }; } private static double EvaluateTrackDifficultyGrowth(float difficulty, double min, double max) { float t = DifficultyToUnit(difficulty); return Lerp(max, min, SmoothStep(t)); } private static double EvaluateTrackDifficultyCap(float difficulty, double min, double max) { float t = DifficultyToUnit(difficulty); return Lerp(max, min, SmoothStep(t)); } private static double EvaluateFrontLoaded(int stageClear, double min, double max) { float t = Mathf.Clamp01(1f - (float)stageClear / 4f); return Clamp(Lerp(1.0, max, t), min, max); } private static double EvaluateBackLoaded(int stageClear, double min, double max) { float num = (float)stageClear % 6f / 6f; float num2 = 0.5f + 0.5f * Mathf.Sin(num * MathF.PI * 2f); float num3 = Mathf.Clamp01((float)stageClear / 8f); float t = Mathf.Clamp01(num2 * 0.65f + num3 * 0.35f); return Lerp(min, max, t); } private static float DifficultyToUnit(float difficulty) { return Mathf.Clamp01((difficulty - 1f) / 12f); } private static float SmoothStep(float t) { t = Mathf.Clamp01(t); return t * t * (3f - 2f * t); } private static double Lerp(double a, double b, float t) { t = Mathf.Clamp01(t); return a + (b - a) * (double)t; } private static double Clamp(double value, double min, double max) { if (value < min) { return min; } if (value > max) { return max; } return value; } private static int ResolvePlayerKey(CharacterBody body) { if ((Object)(object)body == (Object)null) { return 0; } CharacterMaster master = body.master; if ((Object)(object)master != (Object)null) { return ((Object)master).GetInstanceID(); } return ((Object)body).GetInstanceID(); } } internal static class BuildPresetManager { public static void Apply(ExponentialItemsConfig config) { ScalingPreset value = config.ActivePreset.Value; if (value != 0) { List<string> list = new List<string>(); switch (value) { case ScalingPreset.VanillaPlus: SetWithAudit(config, list, config.DefaultScalingMode, ScalingMode.Linear); SetWithAudit(config, list, config.GlobalGrowth, 1.05); break; case ScalingPreset.Chaos: SetWithAudit(config, list, config.DefaultScalingMode, ScalingMode.Exponential); SetWithAudit(config, list, config.GlobalGrowth, 1.4); break; case ScalingPreset.ProcHell: SetWithAudit(config, list, config.DefaultScalingMode, ScalingMode.Exponential); SetWithAudit(config, list, config.ProcMultiplier, 1.6); SetWithAudit(config, list, config.MaxProcDepth, 6); break; case ScalingPreset.MMOScaling: SetWithAudit(config, list, config.DefaultScalingMode, ScalingMode.Logarithmic); SetWithAudit(config, list, config.GlobalGrowth, 1.2); SetWithAudit(config, list, config.SoftCapTarget, 300.0); break; case ScalingPreset.Hardcore: SetWithAudit(config, list, config.DefaultScalingMode, ScalingMode.Hyperbolic); SetWithAudit(config, list, config.GlobalGrowth, 0.7); SetWithAudit(config, list, config.SoftCapTarget, 70.0); break; case ScalingPreset.MovementDisabled: SetWithAudit(config, list, config.MovementMultiplier, 0.0); break; } if (list.Count > 0) { string arg = string.Join(";", list); config.LastAppliedPresetInfo.Value = $"## Active preset: {value} (applied at last launch, overwrote {arg})"; config.Log.LogInfo((object)$"[Preset {value}] Applied {list.Count} value(s). See Meta > Last Applied Preset Info in cfg."); } } } private static void SetWithAudit<T>(ExponentialItemsConfig config, List<string> audit, ConfigEntry<T> entry, T newValue) { T value = entry.Value; if (!object.Equals(value, newValue)) { config.Log.LogInfo((object)$"[Preset {config.ActivePreset.Value}] Setting {((ConfigEntryBase)entry).Definition.Key}: {value} -> {newValue}"); audit.Add($"{((ConfigEntryBase)entry).Definition.Key}={value}"); entry.Value = newValue; } } } internal sealed class CategoryRegistry { private readonly ExponentialItemsConfig _config; public CategoryRegistry(ExponentialItemsConfig config) { _config = config; } public ItemCategory Resolve(ItemDef itemDef) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected I4, but got Unknown if ((Object)(object)itemDef == (Object)null) { return ItemCategory.Utility; } if (_config.TryGetCategoryOverride(itemDef, out var category)) { return category; } ItemTag[] tags = itemDef.tags; if (tags != null) { foreach (ItemTag val in tags) { switch (val - 1) { case 1: return ItemCategory.Healing; case 2: return ItemCategory.Utility; case 0: return ItemCategory.Damage; } } } string text = ((Object)itemDef).name ?? string.Empty; if (text.IndexOf("Syringe", StringComparison.OrdinalIgnoreCase) >= 0) { return ItemCategory.Damage; } if (VanillaUnsafeItemCatalog.TryGetProfile(itemDef, out var profile) && (profile.Flags & UnsafeBehaviorFlags.Movement) != 0) { return ItemCategory.Movement; } if (text.IndexOf("Hoof", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Feather", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Quail", StringComparison.OrdinalIgnoreCase) >= 0) { return ItemCategory.Movement; } if (text.IndexOf("Bandolier", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("AlienHead", StringComparison.OrdinalIgnoreCase) >= 0) { return ItemCategory.Cooldown; } if (text.IndexOf("Proc", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Missile", StringComparison.OrdinalIgnoreCase) >= 0) { return ItemCategory.Proc; } if (text.IndexOf("Armor", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("Shield", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("TougherTimes", StringComparison.OrdinalIgnoreCase) >= 0) { return ItemCategory.Defense; } return ItemCategory.Utility; } } internal static class ConfigLimits { public const int AbsoluteStackCeiling = 536870911; public const int RiskOfOptionsSliderMax = 536870911; } internal static class DebugCommands { private static ItemScalingManager _scaling; private static ExponentialItemsConfig _config; private static UnsafeItemRegistry _unsafeRegistry; public static void Register(ItemScalingManager scaling, ExponentialItemsConfig config, UnsafeItemRegistry unsafeRegistry) { _scaling = scaling; _config = config; _unsafeRegistry = unsafeRegistry; } [ConCommand(/*Could not decode attribute arguments.*/)] private static void EbeiPreview(ConCommandArgs args) { try { if (_scaling == null || _config == null) { Debug.Log((object)"[EBEI] Mod not initialized."); return; } if (((ConCommandArgs)(ref args)).Count < 1) { Debug.Log((object)"[EBEI] Usage: ebei_preview ItemInternalName [currentStack]"); return; } string text = ((ConCommandArgs)(ref args))[0]; int result = 0; if (((ConCommandArgs)(ref args)).Count >= 2 && !int.TryParse(((ConCommandArgs)(ref args))[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) { Debug.Log((object)"[EBEI] Invalid currentStack argument."); return; } result = Mathf.Max(0, result); if (!ItemCatalogResolution.TryResolveItemToken(text, out var itemDef)) { List<string> list = ItemCatalogResolution.FindPartialNameMatches(text); Debug.Log((object)("[EBEI] Item not found: '" + text + "'")); if (list.Count > 0) { Debug.Log((object)("[EBEI] Partial matches: " + string.Join(", ", list))); } return; } CharacterBody body = TryGetLocalPlayerBody(); ScalingPlan scalingPlan = _scaling.BuildPlan(itemDef, result, body); bool flag = _unsafeRegistry != null && _unsafeRegistry.IsUnsafe(itemDef); AdaptiveScalingManager adaptiveScaling = _scaling.AdaptiveScaling; if (adaptiveScaling != null && adaptiveScaling.IsLayerActive()) { AdaptiveMultipliers multipliers = adaptiveScaling.GetMultipliers(body); Debug.Log((object)$"[EBEI] Adaptive layer: {multipliers.Growth:F2}x growth, {multipliers.Cap:F2}x cap"); } List<int> list2 = new List<int> { result }; int num = result; for (int i = 0; i < 8; i++) { ScalingPlan scalingPlan2 = _scaling.BuildPlan(itemDef, num, body); if (scalingPlan2.TargetStack <= num) { break; } num = scalingPlan2.TargetStack; list2.Add(num); } StringBuilder stringBuilder = new StringBuilder(); for (int j = 0; j < list2.Count; j++) { if (j > 0) { stringBuilder.Append(" → "); } stringBuilder.Append(list2[j]); } Debug.Log((object)$"[EBEI] {((Object)itemDef).name} | Mode: {scalingPlan.Mode} | Category: {scalingPlan.Category} | Unsafe: {flag}"); Debug.Log((object)$"[EBEI] Stack progression from {result}: {stringBuilder}"); Debug.Log((object)$"[EBEI] At stack {result}: gain +{scalingPlan.EffectiveGain} | multiplier {scalingPlan.EffectiveMultiplier:F2}x"); } catch (Exception ex) { Debug.LogError((object)("[EBEI] ebei_preview failed: " + ex)); } } private static CharacterBody TryGetLocalPlayerBody() { try { for (int i = 0; i < NetworkUser.readOnlyInstancesList.Count; i++) { NetworkUser val = NetworkUser.readOnlyInstancesList[i]; if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).isLocalPlayer) { CharacterMaster master = val.master; return ((Object)(object)master != (Object)null) ? master.GetBody() : null; } } } catch { } return null; } } [BepInPlugin("com.entersackman.evenbetterexponentialitems", "Even Better Exponential Items", "1.7.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class EvenBetterExponentialItemsPlugin : BaseUnityPlugin { public const string PluginGUID = "com.entersackman.evenbetterexponentialitems"; public const string PluginName = "Even Better Exponential Items"; public const string PluginVersion = "1.7.0"; private static ArtifactDef exponentialArtifactDef; private static EvenBetterExponentialItemsPlugin _instance; private ExponentialItemsConfig _config; private ItemScalingManager _scalingManager; private AdaptiveScalingManager _adaptiveScaling; private TooltipManager _tooltipManager; private readonly Dictionary<string, ItemDef> _itemByDescToken = new Dictionary<string, ItemDef>(StringComparer.Ordinal); private static readonly Dictionary<ItemIndex, int> _lastKnownPlayerStacks = new Dictionary<ItemIndex, int>(); private static int _hookDepth; private float _nextStackRefreshTime; private string PluginDirectory => Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? string.Empty; private void Awake() { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Expected O, but got Unknown //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Expected O, but got Unknown //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Expected O, but got Unknown _instance = this; _config = new ExponentialItemsConfig(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger); BuildPresetManager.Apply(_config); UnsafeItemRegistry unsafeRegistry = new UnsafeItemRegistry(_config); _adaptiveScaling = new AdaptiveScalingManager(_config, ((BaseUnityPlugin)this).Logger, IsExponentsArtifactEnabled); _scalingManager = new ItemScalingManager(_config, new ScalingFormulaRegistry(), new CategoryRegistry(_config), unsafeRegistry, new ProcSafetyManager(_config), _adaptiveScaling); _tooltipManager = new TooltipManager(_config, _scalingManager, _adaptiveScaling, unsafeRegistry); RegisterArtifact(); Inventory.GiveItem_ItemIndex_int += new hook_GiveItem_ItemIndex_int(Inventory_GiveItem_ItemIndex_int); GenericPickupController.AttemptGrant += new hook_AttemptGrant(GenericPickupController_AttemptGrant); Language.GetLocalizedStringByToken += new hook_GetLocalizedStringByToken(Language_GetLocalizedStringByToken); CharacterMaster.OnBodyStart += new hook_OnBodyStart(CharacterMaster_OnBodyStart); Run.Start += new hook_Start(Run_Start); SceneDirector.Start += new hook_Start(SceneDirector_Start); RoR2Application.onFixedUpdate += OnFixedUpdateRefreshLocalStacks; DebugCommands.Register(_scalingManager, _config, unsafeRegistry); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded. Artifact registered: {2}", "Even Better Exponential Items", "1.7.0", (Object)(object)exponentialArtifactDef != (Object)null)); RoR2Application.onLoad = (Action)Delegate.Combine(RoR2Application.onLoad, new Action(OnRoR2CatalogReady)); if (_config.DebugLogArtifactList.Value) { RoR2Application.onLoad = (Action)Delegate.Combine(RoR2Application.onLoad, new Action(LogLoadedArtifacts)); } _config.TryRebuildFromCatalog("Awake post-bind"); RiskOfOptionsIntegration.TryRegister(((BaseUnityPlugin)this).Logger, _config); } private void OnRoR2CatalogReady() { _config.RebuildFromCatalog("RoR2Application.onLoad (item catalog ready)"); RebuildTooltipCache(); RiskOfOptionsIntegration.TryRegister(((BaseUnityPlugin)this).Logger, _config); } private void RebuildTooltipCache() { _itemByDescToken.Clear(); int itemCount = ItemCatalog.itemCount; for (int i = 0; i < itemCount; i++) { ItemDef itemDef = ItemCatalog.GetItemDef((ItemIndex)i); if ((Object)(object)itemDef != (Object)null && !string.IsNullOrEmpty(itemDef.descriptionToken)) { _itemByDescToken[itemDef.descriptionToken] = itemDef; } } } private void RegisterArtifact() { //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) LanguageAPI.Add("ARTIFACT_EXPONENTIALITEMS_NAME", "Artifact of Exponents"); LanguageAPI.Add("ARTIFACT_EXPONENTIALITEMS_DESC", "Items stack exponentially instead of one at a time."); exponentialArtifactDef = ScriptableObject.CreateInstance<ArtifactDef>(); exponentialArtifactDef.cachedName = "ExponentialItems"; exponentialArtifactDef.nameToken = "ARTIFACT_EXPONENTIALITEMS_NAME"; exponentialArtifactDef.descriptionToken = "ARTIFACT_EXPONENTIALITEMS_DESC"; Texture2D val = TryLoadTexture("icon_selected.png", "icon.png"); Texture2D val2 = TryLoadTexture("icon_deselected.png"); Sprite smallIconSelectedSprite = (((Object)(object)val != (Object)null) ? CreateSpriteFromTexture(val, "artifact_selected") : CreateFallbackIcon(new Color(0.25f, 0.8f, 0.35f, 1f), "fallback_selected")); Sprite smallIconDeselectedSprite = (((Object)(object)val2 != (Object)null) ? CreateSpriteFromTexture(val2, "artifact_deselected") : ((!((Object)(object)val != (Object)null)) ? CreateFallbackIcon(new Color(0.2f, 0.2f, 0.2f, 1f), "fallback_deselected") : CreateSpriteFromTexture(CreateDarkenedCopy(val, 0.55f), "artifact_deselected_dim"))); exponentialArtifactDef.smallIconSelectedSprite = smallIconSelectedSprite; exponentialArtifactDef.smallIconDeselectedSprite = smallIconDeselectedSprite; ContentAddition.AddArtifactDef(exponentialArtifactDef); } private Texture2D TryLoadTexture(params string[] fileNamesInOrder) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown foreach (string text in fileNamesInOrder) { string text2 = Path.Combine(PluginDirectory, text); if (!File.Exists(text2)) { continue; } try { byte[] array = File.ReadAllBytes(text2); Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); if (!ImageConversion.LoadImage(val, array)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Icon file could not be decoded: " + text2)); continue; } ((Object)val).name = Path.GetFileNameWithoutExtension(text); return val; } catch (Exception arg) { ((BaseUnityPlugin)this).Logger.LogError((object)$"Failed to load icon {text}: {arg}"); } } return null; } private static Sprite CreateSpriteFromTexture(Texture2D texture, string spriteName) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) ((Object)texture).name = spriteName; return Sprite.Create(texture, new Rect(0f, 0f, (float)((Texture)texture).width, (float)((Texture)texture).height), new Vector2(0.5f, 0.5f), 100f); } private static Texture2D CreateDarkenedCopy(Texture2D source, float rgbMultiplier) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) int width = ((Texture)source).width; int height = ((Texture)source).height; Texture2D val = new Texture2D(width, height, (TextureFormat)4, false); ((Object)val).name = ((Object)source).name + "_dark"; Color[] pixels = source.GetPixels(); float num = Mathf.Clamp01(rgbMultiplier); for (int i = 0; i < pixels.Length; i++) { Color val2 = pixels[i]; pixels[i] = new Color(val2.r * num, val2.g * num, val2.b * num, val2.a); } val.SetPixels(pixels); val.Apply(); return val; } private Sprite CreateFallbackIcon(Color color, string name) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) try { Texture2D val = new Texture2D(128, 128, (TextureFormat)4, false); Color[] array = (Color[])(object)new Color[16384]; for (int i = 0; i < array.Length; i++) { array[i] = color; } val.SetPixels(array); val.Apply(); ((Object)val).name = name; return Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f), 100f); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Failed to create fallback icon: " + ex)); return null; } } private void Inventory_GiveItem_ItemIndex_int(orig_GiveItem_ItemIndex_int orig, Inventory self, ItemIndex itemIndex, int count) { //IL_00fe: 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_000a: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) try { if (_hookDepth > 0) { orig.Invoke(self, itemIndex, count); return; } if (!ShouldApply(self, itemIndex, count, out var itemDef)) { orig.Invoke(self, itemIndex, count); return; } CharacterBody component = ((Component)self).GetComponent<CharacterBody>(); int itemCountPermanent = self.GetItemCountPermanent(itemIndex); int num = _scalingManager.ResolveGrantedAmount(itemDef, itemCountPermanent, component); ScalingPlan scalingPlan = _scalingManager.BuildPlan(itemDef, itemCountPermanent, component); LogDebug($"GiveItem {Describe(itemDef)}: mode={scalingPlan.Mode}, category={scalingPlan.Category}, current={scalingPlan.CurrentStack}, target={scalingPlan.TargetStack}, adding={num}, multiplier={scalingPlan.EffectiveMultiplier:F2}"); IncrementHookDepth(); try { orig.Invoke(self, itemIndex, num); } finally { DecrementHookDepth(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); orig.Invoke(self, itemIndex, count); } } private void GenericPickupController_AttemptGrant(orig_AttemptGrant orig, GenericPickupController self, CharacterBody body) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected O, but got Unknown //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Invalid comparison between Unknown and I4 //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) Inventory val = (((Object)(object)body != (Object)null) ? body.inventory : null); PickupDef pickupDef = PickupCatalog.GetPickupDef(self.pickupIndex); ItemIndex val2 = (ItemIndex)((pickupDef == null) ? (-1) : ((int)pickupDef.itemIndex)); int num = 0; if ((Object)val != (Object)null && (int)val2 != -1) { num = val.GetItemCountPermanent(val2); } bool flag = false; try { IncrementHookDepth(); try { orig.Invoke(self, body); flag = true; } finally { DecrementHookDepth(); } if ((Object)val == (Object)null || (int)val2 == -1) { return; } int itemCountPermanent = val.GetItemCountPermanent(val2); _lastKnownPlayerStacks[val2] = itemCountPermanent; if (!ShouldApplyItem(val, val2, out var itemDef) || itemCountPermanent - num != 1) { return; } ScalingPlan scalingPlan = _scalingManager.BuildPlan(itemDef, num, body); int num2 = scalingPlan.TargetStack - itemCountPermanent; if (num2 > 0) { LogDebug($"Pickup {Describe(itemDef)}: mode={scalingPlan.Mode}, category={scalingPlan.Category}, before={num}, after={itemCountPermanent}, target={scalingPlan.TargetStack}, adding={num2}"); IncrementHookDepth(); try { val.GiveItem(val2, num2); } finally { DecrementHookDepth(); } _lastKnownPlayerStacks[val2] = val.GetItemCountPermanent(val2); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); if (!flag) { try { orig.Invoke(self, body); return; } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)ex2); return; } } } } private static void IncrementHookDepth() { _hookDepth++; if (_hookDepth > 3 && (Object)(object)_instance != (Object)null) { ((BaseUnityPlugin)_instance).Logger.LogWarning((object)$"[Even Better Exponential Items] Hook depth {_hookDepth} exceeds 3 — unexpected GiveItem recursion detected."); } } private static void DecrementHookDepth() { if (_hookDepth > 0) { _hookDepth--; } } private void CharacterMaster_OnBodyStart(orig_OnBodyStart orig, CharacterMaster self, CharacterBody body) { try { orig.Invoke(self, body); if (!((Object)(object)body == (Object)null) && ((NetworkBehaviour)body).isLocalPlayer) { RefreshLocalPlayerStacks(body.inventory); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); try { orig.Invoke(self, body); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)ex2); } } } private void OnFixedUpdateRefreshLocalStacks() { try { _adaptiveScaling?.OnFixedUpdate(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); } if (Time.time < _nextStackRefreshTime) { return; } _nextStackRefreshTime = Time.time + 1f; try { for (int i = 0; i < NetworkUser.readOnlyInstancesList.Count; i++) { NetworkUser val = NetworkUser.readOnlyInstancesList[i]; if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).isLocalPlayer) { CharacterMaster master = val.master; CharacterBody val2 = (((Object)(object)master != (Object)null) ? master.GetBody() : null); if ((Object)(object)val2 != (Object)null) { RefreshLocalPlayerStacks(val2.inventory); break; } } } } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)ex2); } } private static void RefreshLocalPlayerStacks(Inventory inventory) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) if ((Object)inventory == (Object)null) { return; } try { int itemCount = ItemCatalog.itemCount; for (int i = 0; i < itemCount; i++) { ItemIndex val = (ItemIndex)i; int itemCountPermanent = inventory.GetItemCountPermanent(val); if (itemCountPermanent > 0) { _lastKnownPlayerStacks[val] = itemCountPermanent; } } } catch { } } private bool ShouldApply(Inventory self, ItemIndex itemIndex, int count, out ItemDef itemDef) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) itemDef = null; if (count != 1) { return false; } if (!ShouldApplyItem(self, itemIndex, out itemDef)) { return false; } return true; } private bool ShouldApplyItem(Inventory self, ItemIndex itemIndex) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) ItemDef itemDef; return ShouldApplyItem(self, itemIndex, out itemDef); } private bool ShouldApplyItem(Inventory self, ItemIndex itemIndex, out ItemDef itemDef) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Expected O, but got Unknown //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown itemDef = null; if ((Object)self == (Object)null || (int)itemIndex == -1) { return false; } if ((Object)exponentialArtifactDef == (Object)null) { return false; } if (_config.RequireArtifactEnabled.Value) { if ((Object)RunArtifactManager.instance == (Object)null) { return false; } if (!RunArtifactManager.instance.IsArtifactEnabled(exponentialArtifactDef)) { return false; } } ItemDef itemDef2 = ItemCatalog.GetItemDef(itemIndex); if ((Object)itemDef2 == (Object)null) { return false; } if (!_scalingManager.ShouldScaleItem(itemDef2)) { if (_config.DebugLogging.Value) { LogDebug("Scaling disabled by registry rules: " + Describe(itemDef2)); } return false; } itemDef = itemDef2; return true; } private string Language_GetLocalizedStringByToken(orig_GetLocalizedStringByToken orig, Language self, string token) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) string text = orig.Invoke(self, token); if (!_config.EnableTooltipInfo.Value || string.IsNullOrEmpty(token) || !token.EndsWith("_DESC", StringComparison.Ordinal)) { return text; } ItemDef val = FindItemDefForDescToken(token); if ((Object)(object)val == (Object)null || !_scalingManager.ShouldScaleItem(val)) { return text; } int currentStack = 0; if (_lastKnownPlayerStacks.TryGetValue(val.itemIndex, out var value)) { currentStack = value; } CharacterBody body = TryGetLocalPlayerBody(); return text + _tooltipManager.BuildTooltipSuffix(val, currentStack, body); } private static CharacterBody TryGetLocalPlayerBody() { try { for (int i = 0; i < NetworkUser.readOnlyInstancesList.Count; i++) { NetworkUser val = NetworkUser.readOnlyInstancesList[i]; if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).isLocalPlayer) { CharacterMaster master = val.master; return ((Object)(object)master != (Object)null) ? master.GetBody() : null; } } } catch { } return null; } private void Run_Start(orig_Start orig, Run self) { try { orig.Invoke(self); _adaptiveScaling?.OnRunStart(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); try { orig.Invoke(self); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)ex2); } } } private void SceneDirector_Start(orig_Start orig, SceneDirector self) { try { orig.Invoke(self); _adaptiveScaling?.OnStageStart(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); try { orig.Invoke(self); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)ex2); } } } private bool IsExponentsArtifactEnabled() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if ((Object)exponentialArtifactDef == (Object)null) { return false; } if ((Object)RunArtifactManager.instance == (Object)null) { return false; } return RunArtifactManager.instance.IsArtifactEnabled(exponentialArtifactDef); } private ItemDef FindItemDefForDescToken(string token) { if (_itemByDescToken.Count == 0) { RebuildTooltipCache(); } if (!_itemByDescToken.TryGetValue(token, out var value)) { return null; } return value; } private void LogLoadedArtifacts() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Total Artifacts Loaded: {ArtifactCatalog.artifactCount}"); for (int i = 0; i < ArtifactCatalog.artifactCount; i++) { ArtifactDef artifactDef = ArtifactCatalog.GetArtifactDef((ArtifactIndex)i); if ((Object)artifactDef != (Object)null) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Artifact: " + artifactDef.cachedName)); } } } private void LogDebug(string message) { if (_config != null && _config.DebugLogging.Value) { Debug.Log((object)("[Even Better Exponential Items] " + message)); } } private static string Describe(ItemDef def) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected I4, but got Unknown if ((Object)def == (Object)null) { return "(null ItemDef)"; } return $"{((Object)def).name} (index={(int)def.itemIndex})"; } } public enum ScalingMode { Linear, Exponential, Logarithmic, Hyperbolic, SoftCap, Vanilla } public enum ScalingPreset { Custom, VanillaPlus, Chaos, ProcHell, MMOScaling, Hardcore, MovementDisabled } public enum ItemCategory { Damage, Defense, Healing, Utility, Proc, Movement, Cooldown } public sealed class ItemScalingOverride { public ScalingMode Mode; public double Growth; public double Cap; } public sealed class ExponentialItemsConfig { public const string SectionGeneralScaling = "General Scaling"; public const string SectionItemFilters = "Item Filters"; public const string SectionLists = "Lists & Blacklist"; public const string SectionCategoryInfluence = "Category Influence"; public const string SectionRiskyItems = "Risky Items"; public const string SectionProcCombat = "Proc & Combat Limits"; public const string SectionAdaptiveScaling = "Adaptive Scaling"; public const string SectionTooltips = "Tooltips"; public const string SectionAdvanced = "Advanced"; public const string SectionDebug = "Debug"; public const string SectionMeta = "Meta"; public readonly ConfigEntry<bool> RequireArtifactEnabled; public readonly ConfigEntry<int> MaxStack; public readonly ConfigEntry<int> BaseSize; public readonly ConfigEntry<ScalingMode> DefaultScalingMode; public readonly ConfigEntry<double> GlobalGrowth; public readonly ConfigEntry<double> SoftCapTarget; public readonly ConfigEntry<ScalingPreset> ActivePreset; public readonly ConfigEntry<bool> EnablePerItemOverrides; public readonly ConfigEntry<bool> AffectTier1; public readonly ConfigEntry<bool> AffectTier2; public readonly ConfigEntry<bool> AffectTier3; public readonly ConfigEntry<bool> AffectBoss; public readonly ConfigEntry<bool> AffectLunar; public readonly ConfigEntry<bool> AffectVoid; public readonly ConfigEntry<bool> AffectTierless; public readonly ConfigEntry<bool> AffectTemporaryItems; public readonly ConfigEntry<string> BlacklistedItemsRaw; public readonly ConfigEntry<bool> WhitelistMode; public readonly ConfigEntry<string> WhitelistItemsRaw; public readonly ConfigEntry<string> CategoryBlacklistRaw; public readonly ConfigEntry<string> CategoryOverridesRaw; public readonly ConfigEntry<string> PerItemScalingOverridesRaw; public readonly ConfigEntry<string> PerItemStackOverridesRaw; public readonly ConfigEntry<double> MovementMultiplier; public readonly ConfigEntry<double> DefenseMultiplier; public readonly ConfigEntry<double> ProcMultiplier; public readonly ConfigEntry<double> HealingMultiplier; public readonly ConfigEntry<double> UtilityMultiplier; public readonly ConfigEntry<double> CooldownMultiplier; public readonly ConfigEntry<double> DamageMultiplier; public readonly ConfigEntry<double> CategoryExponent; public readonly ConfigEntry<string> UnsafeItemsRaw; public readonly ConfigEntry<bool> UnsafeUseManualList; public readonly ConfigEntry<bool> UseVanillaUnsafeCatalog; public readonly ConfigEntry<UnsafeCatalogSensitivity> RiskCatalogSensitivity; public readonly ConfigEntry<bool> UnsafeRespectCatalogScalingModes; public readonly ConfigEntry<bool> UnsafeAutomaticLegacyPatterns; public readonly ConfigEntry<ScalingMode> UnsafeDefaultMode; public readonly ConfigEntry<bool> UnsafeUseDiminishingReturns; public readonly ConfigEntry<double> UnsafeSoftCapTarget; public readonly ConfigEntry<int> UnsafeHardCap; public readonly ConfigEntry<bool> TooltipShowRiskAudit; public readonly ConfigEntry<int> MaxProcDepth; public readonly ConfigEntry<int> MaxProjectilesPerFrame; public readonly ConfigEntry<float> MaxVelocity; public readonly ConfigEntry<float> MaxAttackSpeed; public readonly ConfigEntry<double> MaxDamageCoefficient; public readonly ConfigEntry<bool> EnableTooltipInfo; public readonly ConfigEntry<bool> AdaptiveScalingEnabled; public readonly ConfigEntry<AdaptiveScalingCurve> AdaptiveCurveShape; public readonly ConfigEntry<double> AdaptiveGrowthMin; public readonly ConfigEntry<double> AdaptiveGrowthMax; public readonly ConfigEntry<double> AdaptiveCapMin; public readonly ConfigEntry<double> AdaptiveCapMax; public readonly ConfigEntry<bool> AdaptiveTeleporterBurst; public readonly ConfigEntry<float> AdaptiveTeleporterBurstSeconds; public readonly ConfigEntry<bool> AdaptiveComebackScaling; public readonly ConfigEntry<float> AdaptiveComebackHpFraction; public readonly ConfigEntry<float> AdaptiveComebackSeconds; public readonly ConfigEntry<double> AdaptiveComebackMaxBonus; public readonly ConfigEntry<bool> AdaptiveMultiplayerEqualization; public readonly ConfigEntry<double> AdaptiveEqualizationStrength; public readonly ConfigEntry<bool> AdaptiveRequireArtifact; public readonly ConfigEntry<bool> LiveReloadLists; public readonly ConfigEntry<bool> DeterministicRounding; public readonly ConfigEntry<bool> DebugLogging; public readonly ConfigEntry<bool> DebugLogArtifactList; public readonly ConfigEntry<bool> DebugLogCatalogRebuild; public readonly ConfigEntry<string> LastAppliedPresetInfo; private readonly ManualLogSource _log; private readonly HashSet<string> _warnedTokens = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<int> _blacklistedIndices = new HashSet<int>(); private readonly HashSet<int> _whitelistedIndices = new HashSet<int>(); private readonly HashSet<ItemCategory> _categoryBlacklist = new HashSet<ItemCategory>(); private readonly HashSet<int> _manualExtraUnsafeIndices = new HashSet<int>(); private readonly Dictionary<int, int> _stackCapByItemIndex = new Dictionary<int, int>(); private readonly Dictionary<int, ItemScalingOverride> _scalingOverridesByItem = new Dictionary<int, ItemScalingOverride>(); private readonly Dictionary<int, ItemCategory> _categoryOverridesByIndex = new Dictionary<int, ItemCategory>(); internal ManualLogSource Log => _log; public ExponentialItemsConfig(ConfigFile config, ManualLogSource log) { _log = log ?? throw new ArgumentNullException("log"); RequireArtifactEnabled = config.Bind<bool>("General Scaling", "Require Exponents Artifact", true, "When ON, this mod only changes how items stack while the Artifact of Exponents is enabled for the run. Turn OFF if you want faster stacking without enabling that artifact."); MaxStack = config.Bind<int>("General Scaling", "Maximum Stack Count", 4096, "Universal ceiling on stacks (in stacks) for ALL items after scaling math. Per-item caps in \"Per-Item Stack Hard Caps\" can lower this further. UnsafeHardCap is an additional ceiling applied only to risky items and evaluated first. Maximum ~536,870,911 (int.MaxValue / 4) for math safety."); BaseSize = config.Bind<int>("General Scaling", "Exponential Base Size", 2, "Log/exponent base (minimum 2) used by Exponential and Logarithmic scaling modes. Higher = bigger jumps between milestones in Exponential mode."); DefaultScalingMode = config.Bind<ScalingMode>("General Scaling", "Default Scaling Mode", ScalingMode.Exponential, "Default growth curve for items that are not \"risky\" and do not have a per-item override.\n\nVanilla — Same as Linear (+1 per pickup); use Vanilla in per-item overrides as a readable \"disable scaling\" marker.\n\nLinear — +1 stack per qualifying pickup.\n\nExponential — Proportional jumps (uses Exponential Base Size and Global Growth).\n\nLogarithmic — Gains based on log curve × Global Growth (uses Exponential Base Size as log base).\n\nSoftCap — Fast early growth that eases toward a plateau (uses Soft Cap Plateau Target in stacks).\n\nHyperbolic — Asymptotic curve toward a plateau (uses Soft Cap Plateau Target in stacks).\n\nTip: With Deterministic +1 Pickup Rounding ON, every valid pickup still moves stacks forward by at least +1."); GlobalGrowth = config.Bind<double>("General Scaling", "Global Growth Multiplier", 1.12, "Unitless multiplier baked into formulas (especially Exponential, Logarithmic, and category-adjusted growth). Typical range ~1.0–1.4."); SoftCapTarget = config.Bind<double>("General Scaling", "Soft Cap Plateau Target", 200.0, "Default plateau width in stacks for SoftCap and Hyperbolic when an item does not set its own cap in overrides. Higher = higher stacks before diminishing kicks in hard."); ActivePreset = config.Bind<ScalingPreset>("General Scaling", "Balance Preset", ScalingPreset.Custom, "Startup preset that adjusts several numbers when you launch (you can still tweak after). Previous values are logged to the BepInEx log and recorded under Meta > Last Applied Preset Info.\n\nCustom — No preset changes.\n\nVanillaPlus — Linear-ish, mild growth.\n\nChaos — Exponential with high growth.\n\nProcHell — Exponential with higher proc strength (still has proc depth guard).\n\nMMOScaling — Logarithmic-leaning with a roomy soft cap.\n\nHardcore — Hyperbolic with a tighter soft cap.\n\nMovementDisabled — Sets Movement Item Strength to 0 so movement buckets stop influencing growth."); EnablePerItemOverrides = config.Bind<bool>("General Scaling", "Enable Per-Item Override Rules", true, "When ON, the \"Per-Item Scaling Rules\" string is parsed. Turn OFF to ignore those rules without deleting them."); AffectTier1 = config.Bind<bool>("Item Filters", "Scale White (Common) Items", true, "Allow white (common) items to use accelerated stacking."); AffectTier2 = config.Bind<bool>("Item Filters", "Scale Green (Uncommon) Items", true, "Allow green (uncommon) items."); AffectTier3 = config.Bind<bool>("Item Filters", "Scale Red (Legendary) Items", true, "Allow red (legendary) items."); AffectBoss = config.Bind<bool>("Item Filters", "Scale Boss (Yellow) Items", true, "Allow boss (yellow) items."); AffectLunar = config.Bind<bool>("Item Filters", "Scale Lunar Items", true, "Allow lunar items."); AffectVoid = config.Bind<bool>("Item Filters", "Scale Void Items", true, "Allow void-tier items."); AffectTierless = config.Bind<bool>("Item Filters", "Scale Hidden / No-Tier Items", false, "Allow hidden/no-tier items (risky; off by default)."); AffectTemporaryItems = config.Bind<bool>("Item Filters", "Scale Temporary Runtime Items", false, "Allow runtime temporary items (usually off)."); BlacklistedItemsRaw = config.Bind<string>("Lists & Blacklist", "Items: Skip Scaling Entirely", string.Empty, "Internal item code names (comma or semicolon), NOT display titles. Those items never use this mod's accelerated stacking (always +1). Leave empty if you only want to tame items via the Risk Catalog instead."); WhitelistMode = config.Bind<bool>("Lists & Blacklist", "Whitelist Mode (Only Listed Items Scale)", false, "When ON, ONLY items listed under Whitelist Item Codes get accelerated stacking; everything else stays vanilla +1 for this mod."); WhitelistItemsRaw = config.Bind<string>("Lists & Blacklist", "Whitelist Item Codes", string.Empty, "Same format as blacklist. Only used when Whitelist Mode is ON."); CategoryBlacklistRaw = config.Bind<string>("Lists & Blacklist", "Category Blacklist", string.Empty, "Skip entire categories from accelerated stacking. Tokens (case-insensitive): Damage, Defense, Healing, Utility, Proc, Movement, Cooldown.\n\nExample: Movement,Proc"); CategoryOverridesRaw = config.Bind<string>("Lists & Blacklist", "Per-Item Category Overrides", string.Empty, "Override which category an item is classified as. Format: ItemCode=CategoryName pairs, comma or semicolon separated. Valid categories: Damage, Defense, Healing, Utility, Proc, Movement, Cooldown. Example: SomeModItem=Movement;AnotherItem=Proc"); PerItemScalingOverridesRaw = config.Bind<string>("Lists & Blacklist", "Per-Item Scaling Rules (Advanced)", string.Empty, "Advanced: ItemCode.Mode=SoftCap; ItemCode.Growth=1.1; ItemCode.Cap=120 — uses internal names (e.g. LensMakersGlasses)."); PerItemStackOverridesRaw = config.Bind<string>("Lists & Blacklist", "Per-Item Stack Hard Caps", string.Empty, "Advanced: ItemCode=256; OtherItem=64 — per-item stack caps (in stacks) after scaling math."); MovementMultiplier = config.Bind<double>("Category Influence", "Movement Item Strength", 0.35, "Unitless strength multiplier for Movement-tagged items. Lower = less runaway speed scaling; 0 matches Movement Disabled preset for this bucket."); DefenseMultiplier = config.Bind<double>("Category Influence", "Defense Item Strength", 0.8, "Unitless strength multiplier for Defense-like items."); ProcMultiplier = config.Bind<double>("Category Influence", "Proc Item Strength", 1.25, "Unitless strength multiplier for proc/on-hit items. Raising this can worsen lag in extreme builds."); HealingMultiplier = config.Bind<double>("Category Influence", "Healing Item Strength", 1.0, "Unitless strength multiplier for healing-related items."); UtilityMultiplier = config.Bind<double>("Category Influence", "Utility Item Strength", 1.0, "Unitless strength multiplier for broad utility items."); CooldownMultiplier = config.Bind<double>("Category Influence", "Cooldown Item Strength", 0.65, "Unitless strength multiplier for cooldown-reduction style items."); DamageMultiplier = config.Bind<double>("Category Influence", "Damage Item Strength", 1.0, "Unitless strength multiplier for damage-focused items."); CategoryExponent = config.Bind<double>("Category Influence", "Category Influence Exponent", 1.0, "Exponent that amplifies how much category multipliers bend away from 1.0 when blended with Global Growth. Example: multiplier 2.0 at exponent 2.0 becomes 4.0×; multiplier 0.5 at exponent 2.0 becomes 0.25×."); UseVanillaUnsafeCatalog = config.Bind<bool>("Risky Items", "Use Built-In Vanilla Risk Catalog", true, "When ON (recommended), a built-in table classifies vanilla + DLC items by gameplay/systems risk for exponential stacking. Risky items use \"Fallback Risky Scaling Mode\" and optional tighter soft caps unless you override them per item."); RiskCatalogSensitivity = config.Bind<UnsafeCatalogSensitivity>("Risky Items", "Risk Catalog Sensitivity", UnsafeCatalogSensitivity.UnsafeAndAbove, "How many catalog tiers count as \"risky\" for scaling:\n\nExtremeOnly — Only the worst offenders (Gesture, Clover, Lysate, etc.).\n\nUnsafeAndAbove — Default. All Unsafe + Extreme catalog entries.\n\nMildAndAbove — Also includes Mild (crit glasses, many heal/regen items). More items use the risky curve."); UnsafeRespectCatalogScalingModes = config.Bind<bool>("Risky Items", "Use Catalog Recommended Scaling Modes", false, "When ON, risky items use each catalog entry's recommended curve (SoftCap, Hyperbolic, HardCap) instead of always using Fallback Risky Scaling Mode. Advanced — can feel very different from the global default."); UnsafeUseManualList = config.Bind<bool>("Risky Items", "Use Additional Manual Risk List", false, "When ON, parses \"Additional Risky Item Codes\" and merges those items into the risky list. When OFF, that text is ignored."); UnsafeItemsRaw = config.Bind<string>("Risky Items", "Additional Risky Item Codes", string.Empty, "Optional extra internal names to ALWAYS treat as risky (merged on top of the catalog). Only read when \"Use Additional Manual Risk List\" is ON."); UnsafeAutomaticLegacyPatterns = config.Bind<bool>("Risky Items", "Legacy Name Pattern Detection", false, "DANGEROUS for mod compatibility: substring match on internal names (Hoof, Feather, Quail, Syringe, Times). Prefer the built-in catalog for vanilla. Turn ON only if you need fuzzy matching for custom items."); UnsafeDefaultMode = config.Bind<ScalingMode>("Risky Items", "Fallback Risky Scaling Mode", ScalingMode.SoftCap, "Scaling mode for risky items when \"Use Catalog Recommended Scaling Modes\" is OFF (or the item has no catalog mode). Default SoftCap is vanilla-friendly."); UnsafeUseDiminishingReturns = config.Bind<bool>("Risky Items", "Tighter Soft Cap For Risky Items", true, "When ON, risky items use the tighter \"Risky Items Soft Cap Target\" (in stacks) for plateau width unless a per-item override sets Cap."); UnsafeSoftCapTarget = config.Bind<double>("Risky Items", "Risky Items Soft Cap Target", 80.0, "Plateau width in stacks for risky items when Tighter Soft Cap is ON — usually lower than the global Soft Cap Plateau Target."); UnsafeHardCap = config.Bind<int>("Risky Items", "Global Stack Hard Cap (Safety)", 512, "Hard ceiling in stacks applied specifically to items flagged as risky (by catalog or manual list), AFTER the scaling formula runs. Does not affect non-risky items. Separate from Maximum Stack Count, which is a universal ceiling for all items."); MaxProcDepth = config.Bind<int>("Proc & Combat Limits", "Maximum Proc Chain Depth", 8, "Maximum proc chain depth (count) the mod uses when clamping huge single-frame grants. Lower = safer CPU, stricter anti-spam."); MaxProjectilesPerFrame = config.Bind<int>("Proc & Combat Limits", "Max Items Granted Per Frame", 120, "Maximum stacks (count) that can be granted in one frame from a single accelerated pickup burst — prevents single-frame spikes."); MaxVelocity = config.Bind<float>("Proc & Combat Limits", "Velocity Clamp (Guardrail)", 450f, "Velocity clamp in units/s used as a reference guardrail paired with movement/proc tuning (full combat clamping depends on other mods/hooks)."); MaxAttackSpeed = config.Bind<float>("Proc & Combat Limits", "Attack Speed Clamp (Guardrail)", 15f, "Attack speed multiplier ceiling used in the mod's safety tuning metadata."); MaxDamageCoefficient = config.Bind<double>("Proc & Combat Limits", "Damage Coefficient Clamp (Guardrail)", 100000.0, "Damage coefficient ceiling used as a numerical guardrail in scaling safety."); EnableTooltipInfo = config.Bind<bool>("Tooltips", "Show Scaling Hints On Items", true, "When ON, item descriptions can show short lines about scaling mode, estimated gains, and next pickup milestones (informational)."); TooltipShowRiskAudit = config.Bind<bool>("Tooltips", "Show Risk Audit Tags On Items", false, "When ON (and tooltips are ON), also shows catalog risk tier, behavior flags, and suggested hard cap hints for audited items. For advanced users."); AdaptiveScalingEnabled = config.Bind<bool>("Adaptive Scaling", "Enable Adaptive Scaling", false, "When ON, live run state (difficulty, stage progress, optional multiplayer spread) adjusts growth and soft-cap multipliers on top of your existing settings. OFF by default — zero effect on stacking when disabled."); AdaptiveCurveShape = config.Bind<AdaptiveScalingCurve>("Adaptive Scaling", "Curve Shape", AdaptiveScalingCurve.TrackDifficulty, "How the adaptive growth layer bends over a run (cap multiplier always tracks difficulty separately).\n\nTrackDifficulty — Generous early, tightens as difficultyCoefficient climbs (default).\n\nFrontLoaded — Strong boost in early stages that decays quickly; flattens by stage 4.\n\nBackLoaded — Climbs in later stages with a repeating wave; rewards long runs.\n\nFlat — Multiplier stays near 1.0; use with burst/equalization sub-features without bending the curve."); AdaptiveGrowthMin = config.Bind<double>("Adaptive Scaling", "Growth Multiplier Min", 0.75, "Lower bound for the adaptive growth multiplier layer (applied on top of Global Growth / per-item growth)."); AdaptiveGrowthMax = config.Bind<double>("Adaptive Scaling", "Growth Multiplier Max", 1.5, "Upper bound for the adaptive growth multiplier layer."); AdaptiveCapMin = config.Bind<double>("Adaptive Scaling", "Cap Multiplier Min", 0.5, "Lower bound for the adaptive soft-cap multiplier layer (always uses TrackDifficulty vs difficulty, regardless of Curve Shape)."); AdaptiveCapMax = config.Bind<double>("Adaptive Scaling", "Cap Multiplier Max", 2.0, "Upper bound for the adaptive soft-cap multiplier layer."); AdaptiveTeleporterBurst = config.Bind<bool>("Adaptive Scaling", "Teleporter Burst After Boss", true, "When ON and adaptive scaling is active, completing a teleporter event spikes growth multiplier to max for the duration below — rewarding post-boss pickups."); AdaptiveTeleporterBurstSeconds = config.Bind<float>("Adaptive Scaling", "Teleporter Burst Duration Seconds", 30f, "How long (seconds) the teleporter burst keeps growth at the adaptive maximum after a boss teleporter finishes."); AdaptiveComebackScaling = config.Bind<bool>("Adaptive Scaling", "Comeback Scaling", false, "When ON, players who stay below the HP threshold for long enough get a small growth multiplier bonus (resets each stage)."); AdaptiveComebackHpFraction = config.Bind<float>("Adaptive Scaling", "Comeback HP Fraction Threshold", 0.35f, "HP fraction (0–1) of max health; at or below this for the comeback duration triggers comeback bonus accumulation."); AdaptiveComebackSeconds = config.Bind<float>("Adaptive Scaling", "Comeback Seconds Below Threshold", 5f, "Seconds continuously at or below the comeback HP threshold before the growth bonus applies."); AdaptiveComebackMaxBonus = config.Bind<double>("Adaptive Scaling", "Comeback Max Growth Bonus", 0.25, "Maximum additive growth bonus from comeback scaling (e.g. 0.25 = up to +25% on the growth multiplier)."); AdaptiveMultiplayerEqualization = config.Bind<bool>("Adaptive Scaling", "Multiplayer Item Equalization", false, "When ON in multiplayer, nudges each player's growth multiplier based on total item stacks vs the group average. No effect in solo."); AdaptiveEqualizationStrength = config.Bind<double>("Adaptive Scaling", "Equalization Strength", 0.15, "How strongly multiplayer equalization adjusts growth per stack difference from the group average (typical 0.05–0.3)."); AdaptiveRequireArtifact = config.Bind<bool>("Adaptive Scaling", "Require Exponents Artifact For Adaptive", false, "When ON, adaptive scaling only runs while the Artifact of Exponents is enabled — independent of the main \"Require Exponents Artifact\" toggle."); LiveReloadLists = config.Bind<bool>("Advanced", "Live Reload Lists From Disk", true, "When ON, editing the cfg file can refresh blacklist / overrides without restarting (when the game is in a good state)."); DeterministicRounding = config.Bind<bool>("Advanced", "Deterministic +1 Pickup Rounding", true, "When ON, each qualifying pickup still moves stacks by at least +1 toward the computed target."); DebugLogging = config.Bind<bool>("Debug", "Verbose Debug Logging", false, "Spammy logs for troubleshooting."); DebugLogArtifactList = config.Bind<bool>("Debug", "Log All Artifacts On Startup", false, "Logs artifact names on startup."); DebugLogCatalogRebuild = config.Bind<bool>("Debug", "Log Catalog Rebuild Details", false, "Logs when item rules are rebuilt from the catalog."); LastAppliedPresetInfo = config.Bind<string>("Meta", "Last Applied Preset Info", string.Empty, "Read-only audit trail: which config keys a balance preset changed at last launch and their previous values (semicolon-delimited)."); if (LiveReloadLists.Value) { BlacklistedItemsRaw.SettingChanged += delegate { TryRebuildFromCatalog("BlacklistedItems changed"); }; WhitelistItemsRaw.SettingChanged += delegate { TryRebuildFromCatalog("WhitelistItems changed"); }; CategoryBlacklistRaw.SettingChanged += delegate { TryRebuildFromCatalog("CategoryBlacklist changed"); }; PerItemScalingOverridesRaw.SettingChanged += delegate { TryRebuildFromCatalog("ItemOverrides changed"); }; PerItemStackOverridesRaw.SettingChanged += delegate { TryRebuildFromCatalog("StackOverrides changed"); }; CategoryOverridesRaw.SettingChanged += delegate { TryRebuildFromCatalog("CategoryOverrides changed"); }; UnsafeItemsRaw.SettingChanged += delegate { TryRebuildFromCatalog("UnsafeItems changed"); }; UnsafeUseManualList.SettingChanged += delegate { TryRebuildFromCatalog("Additional manual risky list toggle changed"); }; UnsafeAutomaticLegacyPatterns.SettingChanged += delegate { TryRebuildFromCatalog("Legacy unsafe patterns changed"); }; UseVanillaUnsafeCatalog.SettingChanged += delegate { TryRebuildFromCatalog("Vanilla unsafe catalog toggle changed"); }; RiskCatalogSensitivity.SettingChanged += delegate { TryRebuildFromCatalog("Unsafe catalog sensitivity changed"); }; WhitelistMode.SettingChanged += delegate { TryRebuildFromCatalog("WhitelistMode changed"); }; } } public void ParseBlacklist() { _blacklistedIndices.Clear(); ParseNameList(BlacklistedItemsRaw.Value, _blacklistedIndices, "blacklist"); } public void ParseWhitelist() { _whitelistedIndices.Clear(); ParseNameList(WhitelistItemsRaw.Value, _whitelistedIndices, "whitelist"); } public void ParseUnsafeItems() { _manualExtraUnsafeIndices.Clear(); if (UnsafeUseManualList.Value) { ParseNameList(UnsafeItemsRaw.Value, _manualExtraUnsafeIndices, "manual-risky"); } } public void ParseCategoryBlacklist() { _categoryBlacklist.Clear(); foreach (string item in SplitListTokens(CategoryBlacklistRaw.Value)) { if (Enum.TryParse<ItemCategory>(item, ignoreCase: true, out var result)) { _categoryBlacklist.Add(result); } else { LogWarningOncePerRebuild("[category-blacklist] Unknown category token: '" + item + "'"); } } } public void ParseCategoryOverrides() { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Expected I4, but got Unknown _categoryOverridesByIndex.Clear(); if (string.IsNullOrWhiteSpace(CategoryOverridesRaw.Value)) { return; } foreach (string item in SplitListTokens(CategoryOverridesRaw.Value)) { int num = item.IndexOf('='); if (num <= 0 || num >= item.Length - 1) { LogWarningOncePerRebuild("[category-overrides] Invalid entry (expected ItemCode=CategoryName): '" + item + "'"); continue; } string text = item.Substring(0, num).Trim(); string text2 = item.Substring(num + 1).Trim(); ItemDef itemDef; if (!Enum.TryParse<ItemCategory>(text2, ignoreCase: true, out var result)) { LogWarningOncePerRebuild("[category-overrides] Unknown category for '" + text + "': '" + text2 + "'"); } else if (!ItemCatalogResolution.TryResolveItemToken(text, out itemDef)) { LogWarningOncePerRebuild("[category-overrides] Unknown or unloaded item token: '" + text + "'"); } else { _categoryOverridesByIndex[(int)itemDef.itemIndex] = result; if (DebugLogCatalogRebuild.Value) { _log.LogInfo((object)$"[category-overrides] {DescribeItem(itemDef)} -> {result}"); } } } } public bool TryGetCategoryOverride(ItemDef def, out ItemCategory category) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected I4, but got Unknown category = ItemCategory.Utility; if ((Object)(object)def != (Object)null) { return _categoryOverridesByIndex.TryGetValue((int)def.itemIndex, out category); } return false; } private void ParseNameList(string raw, HashSet<int> target, string labelForLog) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected I4, but got Unknown if (string.IsNullOrWhiteSpace(raw)) { return; } foreach (string item in SplitListTokens(raw)) { if (!ItemCatalogResolution.TryResolveItemToken(item, out var itemDef)) { LogWarningOncePerRebuild("[" + labelForLog + "] Unknown or unloaded item token: '" + item + "'"); } else { target.Add((int)itemDef.itemIndex); if (DebugLogCatalogRebuild.Value) { _log.LogInfo((object)("[" + labelForLog + "] Registered " + DescribeItem(itemDef))); } } } } public void ParseStackOverrides() { //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Expected I4, but got Unknown _stackCapByItemIndex.Clear(); string value = PerItemStackOverridesRaw.Value; if (string.IsNullOrWhiteSpace(value)) { return; } foreach (string item in SplitListTokens(value)) { int num = item.IndexOf('='); if (num <= 0 || num >= item.Length - 1) { LogWarningOncePerRebuild("[Overrides] Invalid entry (expected Name=Number): '" + item + "'"); continue; } string text = item.Substring(0, num).Trim(); string text2 = item.Substring(num + 1).Trim(); if (!int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { LogWarningOncePerRebuild("[Overrides] Invalid number for '" + text + "': '" + text2 + "'"); continue; } result = ClampPositiveStackCap(result); if (!ItemCatalogResolution.TryResolveItemToken(text, out var itemDef)) { LogWarningOncePerRebuild("[Overrides] Unknown or unloaded item: '" + text + "'"); continue; } _stackCapByItemIndex[(int)itemDef.itemIndex] = result; if (DebugLogCatalogRebuild.Value) { _log.LogInfo((object)$"[Overrides] {DescribeItem(itemDef)} effective max stack = {result}"); } } } public void ParsePerItemScalingOverrides() { //IL_01a8: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Expected I4, but got Unknown _scalingOverridesByItem.Clear(); if (!EnablePerItemOverrides.Value) { return; } Dictionary<string, ItemScalingOverride> dictionary = new Dictionary<string, ItemScalingOverride>(StringComparer.OrdinalIgnoreCase); foreach (string item in SplitListTokens(PerItemScalingOverridesRaw.Value)) { int num = item.IndexOf('='); int num2 = item.IndexOf('.'); if (num <= 0 || num2 <= 0 || num2 >= num) { LogWarningOncePerRebuild("[ItemOverrides] Invalid entry: '" + item + "'"); continue; } string text = item.Substring(0, num2).Trim(); string text2 = item.Substring(num2 + 1, num - num2 - 1).Trim(); string value = item.Substring(num + 1).Trim(); if (text.Length != 0 && text2.Length != 0) { if (!dictionary.TryGetValue(text, out var value2)) { value2 = (dictionary[text] = new ItemScalingOverride { Mode = DefaultScalingMode.Value, Growth = Math.Max(0.0001, GlobalGrowth.Value), Cap = Math.Max(1.0, SoftCapTarget.Value) }); } ApplyOverrideField(value2, text, text2, value); } } foreach (KeyValuePair<string, ItemScalingOverride> item2 in dictionary) { if (!ItemCatalogResolution.TryResolveItemToken(item2.Key, out var itemDef)) { LogWarningOncePerRebuild("[ItemOverrides] Unknown or unloaded item token: '" + item2.Key + "'"); } else { _scalingOverridesByItem[(int)itemDef.itemIndex] = item2.Value; } } } private void ApplyOverrideField(ItemScalingOverride rule, string itemToken, string field, string value) { switch (field.ToLowerInvariant()) { case "mode": { if (Enum.TryParse<ScalingMode>(value, ignoreCase: true, out var result3)) { rule.Mode = result3; break; } LogWarningOncePerRebuild("[ItemOverrides] Invalid mode for '" + itemToken + "': '" + value + "'"); break; } case "growth": { if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { rule.Growth = Math.Max(0.0001, result2); break; } LogWarningOncePerRebuild("[ItemOverrides] Invalid growth for '" + itemToken + "': '" + value + "'"); break; } case "cap": { if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { rule.Cap = Math.Max(1.0, result); break; } LogWarningOncePerRebuild("[ItemOverrides] Invalid cap for '" + itemToken + "': '" + value + "'"); break; } default: LogWarningOncePerRebuild("[ItemOverrides] Unknown field for '" + itemToken + "': '" + field + "'"); break; } } private static IEnumerable<string> SplitListTokens(string raw) { if (string.IsNullOrEmpty(raw)) { yield break; } char[] separator = new char[4] { ';', ',', '\n', '\r' }; string[] array = raw.Split(separator, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { yield return text; } } } private static int ClampPositiveStackCap(int value) { if (value < 1) { return 1; } if (value > 536870911) { return 536870911; } return value; } public bool TryGetItemOverride(ItemDef def, out int maxStackCap) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected I4, but got Unknown maxStackCap = GetGlobalMaxStackClamped(); if ((Object)(object)def == (Object)null) { return false; } if (_stackCapByItemIndex.TryGetValue((int)def.itemIndex, out var value)) { maxStackCap = value; return true; } return false; } public int GetGlobalMaxStackClamped() { return ClampPositiveStackCap(MaxStack.Value); } public int GetEffectiveUnsafeHardCap() { return ClampPositiveStackCap(UnsafeHardCap.Value); } public int GetEffectiveMaxStack(ItemDef def) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected I4, but got Unknown if ((Object)(object)def == (Object)null) { return GetGlobalMaxStackClamped(); } if (!_stackCapByItemIndex.TryGetValue((int)def.itemIndex, out var value)) { return GetGlobalMaxStackClamped(); } return value; } public bool TryGetScalingOverride(ItemDef def, out ItemScalingOverride itemOverride) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected I4, but got Unknown itemOverride = null; if ((Object)(object)def != (Object)null) { return _scalingOverridesByItem.TryGetValue((int)def.itemIndex, out itemOverride); } return false; } public bool IsManualExtraUnsafeItem(ItemDef def) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected I4, but got Unknown if ((Object)(object)def != (Object)null) { return _manualExtraUnsafeIndices.Contains((int)def.itemIndex); } return false; } public bool IsCategoryBlacklisted(ItemCategory category) { return _categoryBlacklist.Contains(category); } public bool IsItemExponentialBlocked(ItemDef itemDef) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected I4, but got Unknown if ((Object)(object)itemDef == (Object)null) { return true; } int item = (int)itemDef.itemIndex; if (WhitelistMode.Value) { if (_whitelistedIndices.Count != 0) { return !_whitelistedIndices.Contains(item); } return true; } return _blacklistedIndices.Contains(item); } public void RebuildFromCatalog(string reason) { try { _warnedTokens.Clear(); ItemCatalogResolution.InvalidateCache(); ParseBlacklist(); ParseWhitelist(); ParseStackOverrides(); ParseUnsafeItems(); ParseCategoryBlacklist(); ParseCategoryOverrides(); ParsePerItemScalingOverrides(); if (GetEffectiveUnsafeHardCap() > GetGlobalMaxStackClamped()) { _log.LogWarning((object)"[Config] UnsafeHardCap is higher than MaxStack — UnsafeHardCap will never trigger because MaxStack is the lower universal ceiling. Consider reducing UnsafeHardCap below MaxStack."); } if (DebugLogging.Value || DebugLogCatalogRebuild.Value) { _log.LogInfo((object)$"Item catalog rules rebuilt ({reason}). Blacklist: {_blacklistedIndices.Count}, Whitelist: {_whitelistedIndices.Count}, Overrides: {_stackCapByItemIndex.Count}, Manual risky extras: {_manualExtraUnsafeIndices.Count}, ItemRules: {_scalingOverridesByItem.Count}, CategoryOverrides: {_categoryOverridesByIndex.Count}"); } } catch (Exception arg) { _log.LogError((object)$"Failed to rebuild item rules: {arg}"); } } public void TryRebuildFromCatalog(string reason) { try { if (ItemCatalog.itemCount <= 0) { return; } } catch { return; } RebuildFromCatalog(reason); } private void LogWarningOncePerRebuild(string message) { if (_warnedTokens.Add(message)) { _log.LogWarning((object)message); } } public double GetCategoryMultiplier(ItemCategory category) { return category switch { ItemCategory.Movement => Math.Max(0.0, MovementMultiplier.Value), ItemCategory.Defense => Math.Max(0.0, DefenseMultiplier.Value), ItemCategory.Proc => Math.Max(0.0, ProcMultiplier.Value), ItemCategory.Healing => Math.Max(0.0, HealingMultiplier.Value), ItemCategory.Utility => Math.Max(0.0, UtilityMultiplier.Value), ItemCategory.Cooldown => Math.Max(0.0, CooldownMultiplier.Value), _ => Math.Max(0.0, DamageMultiplier.Value), }; } private static string DescribeItem(ItemDef def) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected I4, but got Unknown if ((Object)(object)def == (Object)null) { return "(null)"; } return string.Format("{0} (index={1})", ((Object)def).name ?? "?", (int)def.itemIndex); } } internal static class ExponentialStackMath { public static int GetNextTargetStack(int current, int maxCap, int baseSize, double growth) { if (current <= 0) { return Math.Min(1, maxCap); } if (current >= maxCap) { return maxCap; } double num = 1.0 + ((double)Math.Max(2, baseSize) - 1.0) * Math.Max(0.0001, growth); int val = (int)Math.Ceiling((double)current * num); val = Math.Max(current + 1, val); return Math.Min(maxCap, val); } } public static class ItemCatalogResolution { private static readonly Dictionary<string, string> TokenAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "PaulsGoatHoof", "Hoof" }, { "GoatHoof", "Hoof" }, { "HopooFeather", "Feather" }, { "Hopoo Feather", "Feather" }, { "TougherTimes", "Bear" }, { "Tougher Times", "Bear" }, { "SoldiersSyringe", "Syringe" }, { "SoldierSyringe", "Syringe" }, { "Soldiers Syringe", "Syringe" }, { "LensMakersGlasses", "CritGlasses" }, { "Lens Makers Glasses", "CritGlasses" }, { "Crowbar", "NearbyDamageBonusItem" } }; private static readonly Dictionary<string, ItemDef> _nameToItem = new Dictionary<string, ItemDef>(StringComparer.OrdinalIgnoreCase); private static bool _catalogBuilt; public static void InvalidateCache() { _catalogBuilt = false; _nameToItem.Clear(); } public static bool TryResolveItemToken(string token, out ItemDef itemDef) { itemDef = null; if (string.IsNullOrWhiteSpace(token)) { return false; } token = token.Trim(); if (token.Length == 0) { return false; } EnsureCatalog(); if (_nameToItem.TryGetValue(token, out itemDef)) { return (Object)(object)itemDef != (Object)null; } return false; } public static List<string> FindPartialNameMatches(string token, int maxResults = 12) { List<string> list = new List<string>(); if (string.IsNullOrWhiteSpace(token) || maxResults <= 0) { return list; } EnsureCatalog(); string value = token.Trim(); foreach (KeyValuePair<string, ItemDef> item in _nameToItem) { if (!((Object)(object)item.Value == (Object)null) && item.Key.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(item.Key); if (list.Count >= maxResults) { break; } } } return list; } private static void EnsureCatalog() { if (_catalogBuilt) { return; } _nameToItem.Clear(); try { int itemCount = ItemCatalog.itemCount; for (int i = 0; i < itemCount; i++) { ItemDef itemDef = ItemCatalog.GetItemDef((ItemIndex)i); if (!((Object)(object)itemDef == (Object)null) && !string.IsNullOrEmpty(((Object)itemDef).name)) { _nameToItem[((Object)itemDef).name] = itemDef; } } foreach (KeyValuePair<string, string> tokenAlias in TokenAliases) { if (_nameToItem.TryGetValue(tokenAlias.Value, out var value)) { _nameToItem[tokenAlias.Key] = value; } } } catch { _nameToItem.Clear(); } _catalogBuilt = true; } } internal readonly struct ScalingPlan { public readonly ScalingMode Mode; public readonly ItemCategory Category; public readonly int CurrentStack; public readonly int TargetStack; public readonly int EffectiveGain; public readonly double EffectiveMultiplier; public ScalingPlan(ScalingMode mode, ItemCategory category, int currentStack, int targetStack) { Mode = mode; Category = category; CurrentStack = currentStack; TargetStack = targetStack; EffectiveGain = Math.Max(0, targetStack - currentStack); EffectiveMultiplier = ((currentStack <= 0) ? ((double)targetStack) : ((double)targetStack / (double)currentStack)); } } internal sealed class ItemScalingManager { private readonly ExponentialItemsConfig _config; private readonly ScalingFormulaRegistry _formulaRegistry; private readonly CategoryRegistry _categoryRegistry; private readonly UnsafeItemRegistry _unsafeRegistry; private readonly ProcSafetyManager _procSafety; private readonly AdaptiveScalingManager _adaptiveScaling; public AdaptiveScalingManager AdaptiveScaling => _adaptiveScaling; public ItemScalingManager(ExponentialItemsConfig config, ScalingFormulaRegistry formulaRegistry, CategoryRegistry categoryRegistry, UnsafeItemRegistry unsafeRegistry, ProcSafetyManager procSafety, AdaptiveScalingManager adaptiveScaling) { _config = config; _formulaRegistry = formulaRegistry; _categoryRegistry = categoryRegistry; _unsafeRegistry = unsafeRegistry; _procSafety = procSafety; _adaptiveScaling = adaptiveScaling; } public bool ShouldScaleItem(ItemDef itemDef) { if ((Object)(object)itemDef == (Object)null) { return false; } if (!IsAllowedByTier(itemDef)) { return false; } if (_config.IsItemExponentialBlocked(itemDef)) { return false; } ItemCategory category = _categoryRegistry.Resolve(itemDef); return !_config.IsCategoryBlacklisted(category); } public ScalingPlan BuildPlan(ItemDef itemDef, int currentStack, CharacterBody body = null) { ItemCategory category = _categoryRegistry.Resolve(itemDef); int effectiveMaxStack = _config.GetEffectiveMaxStack(itemDef); ScalingMode mode = ResolveMode(itemDef); double growth = ResolveGrowth(itemDef, category, body); double softCapTarget = ResolveCap(itemDef, body); ScalingRequest request = new ScalingRequest(currentStack, effectiveMaxStack, _config.BaseSize.Value, growth, softCapTarget); int nextStack = _formulaRegistry.GetNextStack(mode, request); nextStack = _procSafety.ClampResultingStack(nextStack); if (_config.DeterministicRounding.Value) { nextStack = Math.Max(currentStack + 1, nextStack); } return new ScalingPlan(mode, category, currentStack, nextStack); } public int ResolveGrantedAmount(ItemDef itemDef, int currentStack, CharacterBody body = null) { int requested = Math.Max(1, BuildPlan(itemDef, currentStack, body).EffectiveGain); return _procSafety.ClampGrantedAmount(requested); } private ScalingMode ResolveMode(ItemDef itemDef) { if (_config.TryGetScalingOverride(itemDef, out var itemOverride)) { return itemOverride.Mode; } if (_unsafeRegistry.IsUnsafe(itemDef)) { if (_config.UnsafeRespectCatalogScalingModes.Value && _unsafeRegistry.TryGetBuiltinProfile(itemDef, out var profile) && profile.Tier != 0) { return profile.RecommendedScaling; } return _config.UnsafeDefaultMode.Value; } return _config.DefaultScalingMode.Value; } private double ResolveGrowth(ItemDef itemDef, ItemCategory category, CharacterBody body) { double num = Math.Max(0.0001, _config.GlobalGrowth.Value); double x = Math.Max(0.0001, _config.GetCategoryMultiplier(category)); double y = Math.Max(0.0001, _config.CategoryExponent.Value); double num2 = Math.Pow(x, y); double num3 = num * num2; if (_config.TryGetScalingOverride(itemDef, out var itemOverride)) { num3 = itemOverride.Growth; } num3 *= GetAdaptiveGrowthMultiplier(body); return Math.Max(0.0001, num3); } private double ResolveCap(ItemDef itemDef, CharacterBody body) { double num = Math.Max(1.0, _config.SoftCapTarget.Value); if (_config.TryGetScalingOverride(itemDef, out var itemOverride)) { num = itemOverride.Cap; } else if (_unsafeRegistry.IsUnsafe(itemDef) && _config.UnsafeUseDiminishingReturns.Value) { num = Math.Max(1.0, _config.UnsafeSoftCapTarget.Value); } num *= GetAdaptiveCapMultiplier(body); return Math.Max(1.0, num); } private double GetAdaptiveGrowthMultiplier(CharacterBody body) { if (_adaptiveScaling == null) { return 1.0; } return _adaptiveScaling.GetGrowthMultiplier(body); } private double GetAdaptiveCapMultiplier(CharacterBody body) { if (_adaptiveScaling == null) { return 1.0; } return _adaptiveScaling.GetCapMultiplier(body); } private bool IsAllowedByTier(ItemDef itemDef) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected I4, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 ItemTier tier = itemDef.tier; switch ((int)tier) { default: if ((int)tier == 1000) { return _config.AffectTemporaryItems.Value; } return true; case 0: return _config.AffectTier1.Value; case 1: return _config.AffectTier2.Value; case 2: return _config.AffectTier3.Value; case 4: return _config.AffectBoss.Value; case 3: return _config.AffectLunar.Value; case 6: case 7: case 8: case 9: return _config.AffectVoid.Value; case 5: return _config.AffectTierless.Value; } } } internal static class PlayerDocs { public const string RequireArtifact = "When ON, this mod only changes how items stack while the Artifact of Exponents is enabled for the run. Turn OFF if you want faster stacking without enabling that artifact."; public const string MaxStack = "Universal ceiling on stacks (in stacks) for ALL items after scaling math. Per-item caps in \"Per-Item Stack Hard Caps\" can lower this further. UnsafeHardCap is an additional ceiling applied only to risky items and evaluated first. Maximum ~536,870,911 (int.MaxValue / 4) for math safety."; public const string BaseSize = "Log/exponent base (minimum 2) used by Exponential and Logarithmic scaling modes. Higher = bigger jumps between milestones in Exponential mode."; public const string DefaultScalingMode = "Default growth curve for items that are not \"risky\" and do not have a per-item override.\n\nVanilla — Same as Linear (+1 per pickup); use Vanilla in per-item overrides as a readable \"disable scaling\" marker.\n\nLinear — +1 stack per qualifying pickup.\n\nExponential — Proportional jumps (uses Exponential Base Size and Global Growth).\n\nLogarithmic — Gains based on log curve × Global Growth (uses Exponential Base Size as log base).\n\nSoftCap — Fast early growth that eases toward a plateau (uses Soft Cap Plateau Target in stacks).\n\nHyperbolic — Asymptotic curve toward a plateau (uses Soft Cap Plateau Target in stacks).\n\nTip: With Deterministic +1 Pickup Rounding ON, every valid pickup still moves stacks forward by at least +1."; public const string GlobalGrowth = "Unitless multiplier baked into formulas (especially Exponential, Logarithmic, and category-adjusted growth). Typical range ~1.0–1.4."; public const string SoftCapTarget = "Default plateau width in stacks for SoftCap and Hyperbolic when an item does not set its own cap in overrides. Higher = higher stacks before diminishing kicks in hard."; public const string ActivePreset = "Startup preset that adjusts several numbers when you launch (you can still tweak after). Previous values are logged to the BepInEx log and recorded under Meta > Last Applied Preset Info.\n\nCustom — No preset changes.\n\nVanillaPlus — Linear-ish, mild growth.\n\nChaos — Exponential with high growth.\n\nProcHell — Exponential with higher proc strength (still has proc depth guard).\n\nMMOScaling — Logarithmic-leaning with a roomy soft cap.\n\nHardcore — Hyperbolic with a tighter soft cap.\n\nMovementDisabled — Sets Movement Item Strength to 0 so movement buckets stop influencing growth."; public const string EnablePerItemOverrides = "When ON, the \"Per-Item Scaling Rules\" string is parsed. Turn OFF to ignore those rules without deleting them."; public const string WhitelistMode = "When ON, ONLY items listed under Whitelist Item Codes get accelerated stacking; everything else stays vanilla +1 for this mod."; public const string BlacklistedItems = "Internal item code names (comma or semicolon), NOT display titles. Those items never use this mod's accelerated stacking (always +1). Leave empty if you only want to tame items via the Risk Catalog instead."; public const string WhitelistItems = "Same format as blacklist. Only used when Whitelist Mode is ON."; public const string CategoryBlacklist = "Skip entire categories from accelerated stacking. Tokens (case-insensitive): Damage, Defense, Healing, Utility, Proc, Movement, Cooldown.\n\nExample: Movement,Proc"; public const string CategoryOverrides = "Override which category an item is classified as. Format: ItemCode=CategoryName pairs, comma or semicolon separated. Valid categories: Damage, Defense, Healing, Utility, Proc, Movement, Cooldown. Example: SomeModItem=Movement;AnotherItem=Proc"; public const string PerItemScalingOverrides = "Advanced: ItemCode.Mode=SoftCap; ItemCode.Growth=1.1; ItemCode.Cap=120 — uses internal names (e.g. LensMakersGlasses)."; public const string PerItemStackOverrides = "Advanced: ItemCode=256; OtherItem=64 — per-item stack caps (in stacks) after scaling math."; public const string MovementMultiplier = "Unitless strength multiplier for Movement-tagged items. Lower = less runaway speed scaling; 0 matches Movement Disabled preset for this bucket."; public const string DefenseMultiplier = "Unitless strength multiplier for Defense-like items."; public const string ProcMultiplier = "Unitless strength multiplier for proc/on-hit items. Raising this can worsen lag in extreme builds."; public const string HealingMultiplier = "Unitless strength multiplier for healing-related items."; public const string UtilityMultiplier = "Unitless strength multiplier for broad utility items."; public const string CooldownMultiplier = "Unitless strength multiplier for cooldown-reduction style items."; public const string DamageMultiplier = "Unitless strength multiplier for damage-focused items."; public const string CategoryExponent = "Exponent that amplifies how much category multipliers bend away from 1.0 when blended with Global Growth. Example: multiplier 2.0 at exponent 2.0 becomes 4.0×; multiplier 0.5 at exponent 2.0 becomes 0.25×."; public const string UseVanillaUnsafeCatalog = "When ON (recommended), a built-in table classifies vanilla + DLC items by gameplay/systems risk for exponential stacking. Risky items use \"Fallback Risky Scaling Mode\" and optional tighter soft caps unless you override them per item."; public const string UnsafeCatalogSensitivity = "How many catalog tiers count as \"risky\" for scaling:\n\nExtremeOnly — Only the worst offenders (Gesture, Clover, Lysate, etc.).\n\nUnsafeAndAbove — Default. All Unsafe + Extreme catalog entries.\n\nMildAndAbove — Also includes Mild (crit glasses, many heal/regen items). More items use the risky curve."; public const string UnsafeRespectCatalogScalingModes = "When ON, risky items use each catalog entry's recommended curve (SoftCap, Hyperbolic, HardCap) instead of always using Fallback Risky Scaling Mode. Advanced — can feel very different from the global default."; public const string UnsafeItems = "Optional extra internal names to ALWAYS treat as risky (merged on top of the catalog). Only read when \"Use Additional Manual Risk List\" is ON."; public const string UnsafeUseManualList = "When ON, parses \"Additional Risky Item Codes\" and merges those items into the risky list. When OFF, that text is ignored."; public const string UnsafeAutomaticLegacyPatterns = "DANGEROUS for mod compatibility: substring match on internal names (Hoof, Feather, Quail, Syringe, Times). Prefer the built-in catalog for vanilla. Turn ON only if you need fuzzy matching for custom items."; public const string UnsafeDefaultMode = "Scaling mode for risky items when \"Use Catalog Recommended Scaling Modes\" is OFF (or the item has no catalog mode). Default SoftCap is vanilla-friendly."; public const string UnsafeUseDiminishingReturns = "When ON, risky items use the tighter \"Risky Items Soft Cap Target\" (in stacks) for plateau width unless a per-item override sets Cap."; public const string UnsafeSoftCapTarget = "Plateau width in stacks for risky items when Tighter Soft Cap is ON — usually lower than the global Soft Cap Plateau Target."; public const string UnsafeHardCap = "Hard ceiling in stacks applied specifically to items flagged as risky (by catalog or manual list), AFTER the scaling formula runs. Does not affect non-risky items. Separate from Maximum Stack Count, which is a universal ceiling for all items."; public const string MaxProcDepth = "Maximum proc chain depth (count) the mod uses when clamping huge single-frame grants. Lower = safer CPU, stricter anti-spam."; public const string MaxProjectilesPerFrame = "Maximum stacks (count) that can be granted in one frame from a single accelerated pickup burst — prevents single-frame spikes."; public const string MaxVelocity = "Velocity clamp in units/s used as a reference guardrail paired with movement/proc tuning (full combat clamping depends on other mods/hooks)."; public const string MaxAttackSpeed = "Attack speed multiplier ceiling used in the mod's safety tuning metadata."; public const string MaxDamageCoefficient = "Damage coefficient ceiling used as a numerical guardrail in scaling safety."; public const string AdaptiveScalingEnabled = "When ON, live run state (difficulty, stage progress, optional multiplayer spread) adjusts growth and soft-cap multipliers on top of your existing settings. OFF by default — zero effect on stacking when disabled."; public const string AdaptiveCurveShape = "How the adaptive growth layer bends over a run (cap multiplier always tracks difficulty separately).\n\nTrackDifficulty — Generous early, tightens as difficultyCoefficient climbs (default).\n\nFrontLoaded — Strong boost in early stages that decays quickly; flattens by stage 4.\n\nBackLoaded — Climbs in later stages with a repeating wave; rewards long runs.\n\nFlat — Multiplier stays near 1.0; use with burst/equalization sub-features without bending the curve."; public const string AdaptiveGrowthMin = "Lower bound