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 _isExponentsArtifactEnabled; private readonly Dictionary _playerState = new Dictionary(); private readonly Dictionary _tickCache = new Dictionary(); private int _cacheFrame = -1; private float _teleporterBurstEndTime; private int _lastObservedStageClearCount = -1; public AdaptiveScalingManager(ExponentialItemsConfig config, ManualLogSource log, Func isExponentsArtifactEnabled) { _config = config; _log = log; _isExponentsArtifactEnabled = isExponentsArtifactEnabled ?? ((Func)(() => 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 $"\nAdaptive: {multipliers.Growth:F2}x growth this stage (stage {num})"; } 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 list = new List(); 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(ExponentialItemsConfig config, List audit, ConfigEntry 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 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 list2 = new List { 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 _itemByDescToken = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary _lastKnownPlayerStacks = new Dictionary(); 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(); 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(); 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 RequireArtifactEnabled; public readonly ConfigEntry MaxStack; public readonly ConfigEntry BaseSize; public readonly ConfigEntry DefaultScalingMode; public readonly ConfigEntry GlobalGrowth; public readonly ConfigEntry SoftCapTarget; public readonly ConfigEntry ActivePreset; public readonly ConfigEntry EnablePerItemOverrides; public readonly ConfigEntry AffectTier1; public readonly ConfigEntry AffectTier2; public readonly ConfigEntry AffectTier3; public readonly ConfigEntry AffectBoss; public readonly ConfigEntry AffectLunar; public readonly ConfigEntry AffectVoid; public readonly ConfigEntry AffectTierless; public readonly ConfigEntry AffectTemporaryItems; public readonly ConfigEntry BlacklistedItemsRaw; public readonly ConfigEntry WhitelistMode; public readonly ConfigEntry WhitelistItemsRaw; public readonly ConfigEntry CategoryBlacklistRaw; public readonly ConfigEntry CategoryOverridesRaw; public readonly ConfigEntry PerItemScalingOverridesRaw; public readonly ConfigEntry PerItemStackOverridesRaw; public readonly ConfigEntry MovementMultiplier; public readonly ConfigEntry DefenseMultiplier; public readonly ConfigEntry ProcMultiplier; public readonly ConfigEntry HealingMultiplier; public readonly ConfigEntry UtilityMultiplier; public readonly ConfigEntry CooldownMultiplier; public readonly ConfigEntry DamageMultiplier; public readonly ConfigEntry CategoryExponent; public readonly ConfigEntry UnsafeItemsRaw; public readonly ConfigEntry UnsafeUseManualList; public readonly ConfigEntry UseVanillaUnsafeCatalog; public readonly ConfigEntry RiskCatalogSensitivity; public readonly ConfigEntry UnsafeRespectCatalogScalingModes; public readonly ConfigEntry UnsafeAutomaticLegacyPatterns; public readonly ConfigEntry UnsafeDefaultMode; public readonly ConfigEntry UnsafeUseDiminishingReturns; public readonly ConfigEntry UnsafeSoftCapTarget; public readonly ConfigEntry UnsafeHardCap; public readonly ConfigEntry TooltipShowRiskAudit; public readonly ConfigEntry MaxProcDepth; public readonly ConfigEntry MaxProjectilesPerFrame; public readonly ConfigEntry MaxVelocity; public readonly ConfigEntry MaxAttackSpeed; public readonly ConfigEntry MaxDamageCoefficient; public readonly ConfigEntry EnableTooltipInfo; public readonly ConfigEntry AdaptiveScalingEnabled; public readonly ConfigEntry AdaptiveCurveShape; public readonly ConfigEntry AdaptiveGrowthMin; public readonly ConfigEntry AdaptiveGrowthMax; public readonly ConfigEntry AdaptiveCapMin; public readonly ConfigEntry AdaptiveCapMax; public readonly ConfigEntry AdaptiveTeleporterBurst; public readonly ConfigEntry AdaptiveTeleporterBurstSeconds; public readonly ConfigEntry AdaptiveComebackScaling; public readonly ConfigEntry AdaptiveComebackHpFraction; public readonly ConfigEntry AdaptiveComebackSeconds; public readonly ConfigEntry AdaptiveComebackMaxBonus; public readonly ConfigEntry AdaptiveMultiplayerEqualization; public readonly ConfigEntry AdaptiveEqualizationStrength; public readonly ConfigEntry AdaptiveRequireArtifact; public readonly ConfigEntry LiveReloadLists; public readonly ConfigEntry DeterministicRounding; public readonly ConfigEntry DebugLogging; public readonly ConfigEntry DebugLogArtifactList; public readonly ConfigEntry DebugLogCatalogRebuild; public readonly ConfigEntry LastAppliedPresetInfo; private readonly ManualLogSource _log; private readonly HashSet _warnedTokens = new HashSet(StringComparer.Ordinal); private readonly HashSet _blacklistedIndices = new HashSet(); private readonly HashSet _whitelistedIndices = new HashSet(); private readonly HashSet _categoryBlacklist = new HashSet(); private readonly HashSet _manualExtraUnsafeIndices = new HashSet(); private readonly Dictionary _stackCapByItemIndex = new Dictionary(); private readonly Dictionary _scalingOverridesByItem = new Dictionary(); private readonly Dictionary _categoryOverridesByIndex = new Dictionary(); internal ManualLogSource Log => _log; public ExponentialItemsConfig(ConfigFile config, ManualLogSource log) { _log = log ?? throw new ArgumentNullException("log"); RequireArtifactEnabled = config.Bind("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("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("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("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("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("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("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("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("Item Filters", "Scale White (Common) Items", true, "Allow white (common) items to use accelerated stacking."); AffectTier2 = config.Bind("Item Filters", "Scale Green (Uncommon) Items", true, "Allow green (uncommon) items."); AffectTier3 = config.Bind("Item Filters", "Scale Red (Legendary) Items", true, "Allow red (legendary) items."); AffectBoss = config.Bind("Item Filters", "Scale Boss (Yellow) Items", true, "Allow boss (yellow) items."); AffectLunar = config.Bind("Item Filters", "Scale Lunar Items", true, "Allow lunar items."); AffectVoid = config.Bind("Item Filters", "Scale Void Items", true, "Allow void-tier items."); AffectTierless = config.Bind("Item Filters", "Scale Hidden / No-Tier Items", false, "Allow hidden/no-tier items (risky; off by default)."); AffectTemporaryItems = config.Bind("Item Filters", "Scale Temporary Runtime Items", false, "Allow runtime temporary items (usually off)."); BlacklistedItemsRaw = config.Bind("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("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("Lists & Blacklist", "Whitelist Item Codes", string.Empty, "Same format as blacklist. Only used when Whitelist Mode is ON."); CategoryBlacklistRaw = config.Bind("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("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("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("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("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("Category Influence", "Defense Item Strength", 0.8, "Unitless strength multiplier for Defense-like items."); ProcMultiplier = config.Bind("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("Category Influence", "Healing Item Strength", 1.0, "Unitless strength multiplier for healing-related items."); UtilityMultiplier = config.Bind("Category Influence", "Utility Item Strength", 1.0, "Unitless strength multiplier for broad utility items."); CooldownMultiplier = config.Bind("Category Influence", "Cooldown Item Strength", 0.65, "Unitless strength multiplier for cooldown-reduction style items."); DamageMultiplier = config.Bind("Category Influence", "Damage Item Strength", 1.0, "Unitless strength multiplier for damage-focused items."); CategoryExponent = config.Bind("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("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("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("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("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("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("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("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("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("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("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("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("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("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("Proc & Combat Limits", "Attack Speed Clamp (Guardrail)", 15f, "Attack speed multiplier ceiling used in the mod's safety tuning metadata."); MaxDamageCoefficient = config.Bind("Proc & Combat Limits", "Damage Coefficient Clamp (Guardrail)", 100000.0, "Damage coefficient ceiling used as a numerical guardrail in scaling safety."); EnableTooltipInfo = config.Bind("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("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("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("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("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("Adaptive Scaling", "Growth Multiplier Max", 1.5, "Upper bound for the adaptive growth multiplier layer."); AdaptiveCapMin = config.Bind("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("Adaptive Scaling", "Cap Multiplier Max", 2.0, "Upper bound for the adaptive soft-cap multiplier layer."); AdaptiveTeleporterBurst = config.Bind("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("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("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("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("Adaptive Scaling", "Comeback Seconds Below Threshold", 5f, "Seconds continuously at or below the comeback HP threshold before the growth bonus applies."); AdaptiveComebackMaxBonus = config.Bind("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("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("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("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("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("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("Debug", "Verbose Debug Logging", false, "Spammy logs for troubleshooting."); DebugLogArtifactList = config.Bind("Debug", "Log All Artifacts On Startup", false, "Logs artifact names on startup."); DebugLogCatalogRebuild = config.Bind("Debug", "Log Catalog Rebuild Details", false, "Logs when item rules are rebuilt from the catalog."); LastAppliedPresetInfo = config.Bind("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(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(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 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 dictionary = new Dictionary(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 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(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 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 TokenAliases = new Dictionary(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 _nameToItem = new Dictionary(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 FindPartialNameMatches(string token, int maxResults = 12) { List list = new List(); if (string.IsNullOrWhiteSpace(token) || maxResults <= 0) { return list; } EnsureCatalog(); string value = token.Trim(); foreach (KeyValuePair 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 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 for the adaptive growth multiplier layer (applied on top of Global Growth / per-item growth)."; public const string AdaptiveGrowthMax = "Upper bound for the adaptive growth multiplier layer."; public const string AdaptiveCapMin = "Lower bound for the adaptive soft-cap multiplier layer (always uses TrackDifficulty vs difficulty, regardless of Curve Shape)."; public const string AdaptiveCapMax = "Upper bound for the adaptive soft-cap multiplier layer."; public const string AdaptiveTeleporterBurst = "When ON and adaptive scaling is active, completing a teleporter event spikes growth multiplier to max for the duration below — rewarding post-boss pickups."; public const string AdaptiveTeleporterBurstSeconds = "How long (seconds) the teleporter burst keeps growth at the adaptive maximum after a boss teleporter finishes."; public const string AdaptiveComebackScaling = "When ON, players who stay below the HP threshold for long enough get a small growth multiplier bonus (resets each stage)."; public const string AdaptiveComebackHpFraction = "HP fraction (0–1) of max health; at or below this for the comeback duration triggers comeback bonus accumulation."; public const string AdaptiveComebackSeconds = "Seconds continuously at or below the comeback HP threshold before the growth bonus applies."; public const string AdaptiveComebackMaxBonus = "Maximum additive growth bonus from comeback scaling (e.g. 0.25 = up to +25% on the growth multiplier)."; public const string AdaptiveMultiplayerEqualization = "When ON in multiplayer, nudges each player's growth multiplier based on total item stacks vs the group average. No effect in solo."; public const string AdaptiveEqualizationStrength = "How strongly multiplayer equalization adjusts growth per stack difference from the group average (typical 0.05–0.3)."; public const string AdaptiveRequireArtifact = "When ON, adaptive scaling only runs while the Artifact of Exponents is enabled — independent of the main \"Require Exponents Artifact\" toggle."; public const string EnableTooltipInfo = "When ON, item descriptions can show short lines about scaling mode, estimated gains, and next pickup milestones (informational)."; public const string TooltipShowRiskAudit = "When ON (and tooltips are ON), also shows catalog risk tier, behavior flags, and suggested hard cap hints for audited items. For advanced users."; public const string LiveReloadLists = "When ON, editing the cfg file can refresh blacklist / overrides without restarting (when the game is in a good state)."; public const string DeterministicRounding = "When ON, each qualifying pickup still moves stacks by at least +1 toward the computed target."; public const string LastAppliedPresetInfo = "Read-only audit trail: which config keys a balance preset changed at last launch and their previous values (semicolon-delimited)."; public const string DebugLogging = "Spammy logs for troubleshooting."; public const string DebugLogArtifactList = "Logs artifact names on startup."; public const string DebugLogCatalogRebuild = "Logs when item rules are rebuilt from the catalog."; public const string AffectTier1 = "Allow white (common) items to use accelerated stacking."; public const string AffectTier2 = "Allow green (uncommon) items."; public const string AffectTier3 = "Allow red (legendary) items."; public const string AffectBoss = "Allow boss (yellow) items."; public const string AffectLunar = "Allow lunar items."; public const string AffectVoid = "Allow void-tier items."; public const string AffectTierless = "Allow hidden/no-tier items (risky; off by default)."; public const string AffectTemporaryItems = "Allow runtime temporary items (usually off)."; } internal sealed class ProcSafetyManager { private readonly ExponentialItemsConfig _config; public ProcSafetyManager(ExponentialItemsConfig config) { _config = config ?? throw new ArgumentNullException("config"); } public int ClampGrantedAmount(int requested) { if (requested < 1) { return 1; } return Math.Min(requested, _config.MaxProjectilesPerFrame.Value); } public int ClampResultingStack(int stack) { return Math.Min(stack, _config.GetEffectiveUnsafeHardCap()); } } internal static class RiskOfOptionsIntegration { private static bool _registered; private static MethodInfo method_AddOption; private static MethodInfo method_SetModDescription; public static void TryRegister(ManualLogSource log, ExponentialItemsConfig c) { if (_registered) { return; } Assembly assembly = FindRiskOfOptionsAssembly(); if (assembly == null) { return; } try { Type type = assembly.GetType("RiskOfOptions.ModSettingsManager"); if (type == null) { log.LogWarning((object)"[EvenBetterExponentialItems] RiskOfOptions.ModSettingsManager not found."); return; } MethodInfo[] source = (from m in type.GetMethods(BindingFlags.Static | BindingFlags.Public) where m.Name == "AddOption" && m.GetParameters().Length == 1 select m).ToArray(); method_AddOption = source.FirstOrDefault((MethodInfo m) => m.GetParameters()[0].ParameterType.FullName == "RiskOfOptions.Options.BaseOption") ?? source.FirstOrDefault(); if (method_AddOption == null) { log.LogWarning((object)"[EvenBetterExponentialItems] RiskOfOptions AddOption API mismatch."); return; } method_SetModDescription = type.GetMethod("SetModDescription", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { typeof(string) }, null); method_SetModDescription?.Invoke(null, new object[1] { "Even Better Exponential Items — faster stacking with guardrails. Start with presets under General Scaling, then open Risky Items if you want to tune the built-in safety catalog. Every option has a tooltip; the same text is in your BepInEx config file under clearer section names." }); AddCheckbox(assembly, c.RequireArtifactEnabled); AddIntSlider(assembly, c.MaxStack, 1, 536870911); AddIntSlider(assembly, c.BaseSize, 2, 10); AddEnumChoice(assembly, c.DefaultScalingMode); AddEnumChoice(assembly, c.ActivePreset); AddCheckbox(assembly, c.EnablePerItemOverrides); TryAddStepSliderDouble(assembly, c.GlobalGrowth, 0.05, 5.0, 0.01); TryAddStepSliderDouble(assembly, c.SoftCapTarget, 10.0, 2000.0, 5.0); AddCheckbox(assembly, c.AffectTier1); AddCheckbox(assembly, c.AffectTier2); AddCheckbox(assembly, c.AffectTier3); AddCheckbox(assembly, c.AffectBoss); AddCheckbox(assembly, c.AffectLunar); AddCheckbox(assembly, c.AffectVoid); AddCheckbox(assembly, c.AffectTierless); AddCheckbox(assembly, c.AffectTemporaryItems); AddCheckbox(assembly, c.WhitelistMode); AddStringField(assembly, c.BlacklistedItemsRaw); AddStringField(assembly, c.WhitelistItemsRaw); AddStringField(assembly, c.CategoryBlacklistRaw); AddStringField(assembly, c.PerItemScalingOverridesRaw); AddStringField(assembly, c.PerItemStackOverridesRaw); TryAddStepSliderDouble(assembly, c.MovementMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.DefenseMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.ProcMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.HealingMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.UtilityMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.CooldownMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.DamageMultiplier, 0.0, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.CategoryExponent, 0.1, 5.0, 0.05); AddCheckbox(assembly, c.UseVanillaUnsafeCatalog); AddEnumChoice(assembly, c.RiskCatalogSensitivity); AddCheckbox(assembly, c.UnsafeRespectCatalogScalingModes); AddCheckbox(assembly, c.UnsafeUseManualList); AddStringField(assembly, c.UnsafeItemsRaw); AddCheckbox(assembly, c.UnsafeAutomaticLegacyPatterns); AddEnumChoice(assembly, c.UnsafeDefaultMode); AddCheckbox(assembly, c.UnsafeUseDiminishingReturns); TryAddStepSliderDouble(assembly, c.UnsafeSoftCapTarget, 5.0, 500.0, 1.0); AddIntSlider(assembly, c.UnsafeHardCap, 1, 536870911); AddIntSlider(assembly, c.MaxProcDepth, 1, 64); AddIntSlider(assembly, c.MaxProjectilesPerFrame, 1, 500); TryAddSliderFloat(assembly, c.MaxVelocity, 50f, 2000f); TryAddSliderFloat(assembly, c.MaxAttackSpeed, 1f, 50f); TryAddStepSliderDouble(assembly, c.MaxDamageCoefficient, 1000.0, 5000000.0, 1000.0); AddCheckbox(assembly, c.EnableTooltipInfo); AddCheckbox(assembly, c.TooltipShowRiskAudit); AddCheckbox(assembly, c.AdaptiveScalingEnabled); AddEnumChoice(assembly, c.AdaptiveCurveShape); TryAddStepSliderDouble(assembly, c.AdaptiveGrowthMin, 0.1, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.AdaptiveGrowthMax, 0.1, 3.0, 0.05); TryAddStepSliderDouble(assembly, c.AdaptiveCapMin, 0.1, 5.0, 0.05); TryAddStepSliderDouble(assembly, c.AdaptiveCapMax, 0.1, 5.0, 0.05); AddCheckbox(assembly, c.AdaptiveTeleporterBurst); TryAddSliderFloat(assembly, c.AdaptiveTeleporterBurstSeconds, 5f, 120f); AddCheckbox(assembly, c.AdaptiveComebackScaling); TryAddSliderFloat(assembly, c.AdaptiveComebackHpFraction, 0.05f, 0.95f); TryAddSliderFloat(assembly, c.AdaptiveComebackSeconds, 1f, 30f); TryAddStepSliderDouble(assembly, c.AdaptiveComebackMaxBonus, 0.0, 1.0, 0.05); AddCheckbox(assembly, c.AdaptiveMultiplayerEqualization); TryAddStepSliderDouble(assembly, c.AdaptiveEqualizationStrength, 0.0, 1.0, 0.01); AddCheckbox(assembly, c.AdaptiveRequireArtifact); AddCheckbox(assembly, c.LiveReloadLists); AddCheckbox(assembly, c.DeterministicRounding); AddCheckbox(assembly, c.DebugLogging); AddCheckbox(assembly, c.DebugLogArtifactList); AddCheckbox(assembly, c.DebugLogCatalogRebuild); _registered = true; log.LogInfo((object)"Risk of Options: in-game settings registered for Even Better Exponential Items."); } catch (Exception ex) { log.LogWarning((object)("Risk of Options integration failed — config file still works. " + ex.Message)); } } private static Assembly FindRiskOfOptionsAssembly() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { if (string.Equals(assembly.GetName().Name, "RiskOfOptions", StringComparison.Ordinal)) { return assembly; } } catch { } } return null; } private static void AddCheckbox(Assembly roo, ConfigEntry entry) { Type type = roo.GetType("RiskOfOptions.Options.CheckBoxOption"); if (!(type == null)) { object obj = Activator.CreateInstance(type, entry); method_AddOption.Invoke(null, new object[1] { obj }); } } private static void AddIntSlider(Assembly roo, ConfigEntry entry, int min, int max) { Type type = roo.GetType("RiskOfOptions.OptionConfigs.IntSliderConfig"); Type type2 = roo.GetType("RiskOfOptions.Options.IntSliderOption"); if (!(type == null) && !(type2 == null)) { object obj = Activator.CreateInstance(type); FieldInfo field = type.GetField("min"); FieldInfo? field2 = type.GetField("max"); field?.SetValue(obj, min); field2?.SetValue(obj, max); object obj2 = Activator.CreateInstance(type2, entry, obj); method_AddOption.Invoke(null, new object[1] { obj2 }); } } private static void TryAddSliderFloat(Assembly roo, ConfigEntry entry, float min, float max) { Type type = roo.GetType("RiskOfOptions.OptionConfigs.SliderConfig"); Type type2 = roo.GetType("RiskOfOptions.Options.SliderOption"); if (type == null || type2 == null) { return; } try { object obj = Activator.CreateInstance(type); type.GetField("min")?.SetValue(obj, min); type.GetField("max")?.SetValue(obj, max); object obj2 = Activator.CreateInstance(type2, entry, obj); method_AddOption.Invoke(null, new object[1] { obj2 }); } catch { } } private static void TryAddStepSliderDouble(Assembly roo, ConfigEntry entry, double min, double max, double step) { Type type = roo.GetType("RiskOfOptions.OptionConfigs.StepSliderConfig"); Type type2 = roo.GetType("RiskOfOptions.Options.StepSliderOption"); if (type == null || type2 == null) { return; } try { object obj = Activator.CreateInstance(type); type.GetField("min")?.SetValue(obj, (float)min); type.GetField("max")?.SetValue(obj, (float)max); type.GetField("increment")?.SetValue(obj, (float)step); object obj2 = Activator.CreateInstance(type2, entry, obj); method_AddOption.Invoke(null, new object[1] { obj2 }); } catch { } } private static void AddStringField(Assembly roo, ConfigEntry entry) { Type type = roo.GetType("RiskOfOptions.Options.StringInputFieldOption"); if (!(type == null)) { object obj = Activator.CreateInstance(type, entry); method_AddOption.Invoke(null, new object[1] { obj }); } } private static void AddEnumChoice(Assembly roo, ConfigEntry entry) where T : struct, IConvertible { Type type = roo.GetType("RiskOfOptions.Options.ChoiceOption"); if (type == null) { return; } try { object obj = Activator.CreateInstance(type, entry); method_AddOption.Invoke(null, new object[1] { obj }); } catch { } } } internal readonly struct ScalingRequest { public readonly int Current; public readonly int MaxCap; public readonly int BaseSize; public readonly double Growth; public readonly double SoftCapTarget; public ScalingRequest(int current, int maxCap, int baseSize, double growth, double softCapTarget) { Current = Math.Max(0, current); MaxCap = Math.Max(1, maxCap); BaseSize = Math.Max(2, baseSize); Growth = Math.Max(0.0001, growth); SoftCapTarget = Math.Max(1.0, softCapTarget); } } internal sealed class ScalingFormulaRegistry { public int GetNextStack(ScalingMode mode, ScalingRequest request) { switch (mode) { case ScalingMode.Linear: case ScalingMode.Vanilla: return Clamp(request.Current + 1, request.MaxCap); case ScalingMode.Logarithmic: return Clamp(request.Current + (int)Math.Max(1.0, Math.Log((double)request.Current + 2.0, Math.Max(2, request.BaseSize)) * request.Growth), request.MaxCap); case ScalingMode.Hyperbolic: { double num2 = (double)request.Current + 1.0; return Clamp((int)Math.Ceiling(request.SoftCapTarget * num2 / (request.SoftCapTarget + num2)), request.MaxCap); } case ScalingMode.SoftCap: { double num = (double)request.Current + 1.0; return Clamp((int)Math.Ceiling(request.SoftCapTarget * (1.0 - Math.Exp((0.0 - num) / request.SoftCapTarget))), request.MaxCap); } default: return ExponentialStackMath.GetNextTargetStack(request.Current, request.MaxCap, request.BaseSize, request.Growth); } } private static int Clamp(int value, int cap) { if (value < 1) { return 1; } if (value > cap) { return cap; } return value; } } internal sealed class TooltipManager { private readonly ExponentialItemsConfig _config; private readonly ItemScalingManager _scaling; private readonly AdaptiveScalingManager _adaptiveScaling; private readonly UnsafeItemRegistry _unsafeRegistry; public TooltipManager(ExponentialItemsConfig config, ItemScalingManager scaling, AdaptiveScalingManager adaptiveScaling, UnsafeItemRegistry unsafeRegistry) { _config = config; _scaling = scaling; _adaptiveScaling = adaptiveScaling; _unsafeRegistry = unsafeRegistry; } public string BuildTooltipSuffix(ItemDef itemDef, int currentStack, CharacterBody body) { if (!_config.EnableTooltipInfo.Value || (Object)(object)itemDef == (Object)null) { return string.Empty; } ScalingPlan scalingPlan = _scaling.BuildPlan(itemDef, currentStack, body); double num = ((scalingPlan.TargetStack <= 0) ? 0.0 : (1.0 - (double)scalingPlan.EffectiveGain / (double)Mathf.Max(1, scalingPlan.TargetStack))); string text = ((currentStack <= 0) ? $"First pickup: 0→{scalingPlan.TargetStack} (+{scalingPlan.EffectiveGain})" : $"{currentStack}→{scalingPlan.TargetStack} (+{scalingPlan.EffectiveGain})"); string text2 = $"\nBase: +1\nScaled: {text}\nMode: {scalingPlan.Mode}\nDiminishing: {num * 100.0:F1}%\nEffective Multiplier: {scalingPlan.EffectiveMultiplier:F2}x"; if (scalingPlan.Mode != ScalingMode.Vanilla && scalingPlan.Mode != 0) { text2 += BuildProgressionPreview(itemDef, currentStack, body); } if (_adaptiveScaling != null) { text2 += _adaptiveScaling.TryFormatTooltipSuffix(body); } if (_config.TooltipShowRiskAudit.Value && _unsafeRegistry.TryGetBuiltinProfile(itemDef, out var profile)) { text2 += $"\nRisk tier: {profile.Tier}\nFlags: {FormatFlags(profile.Flags)}"; if (profile.SuggestedHardStackCap.HasValue) { text2 += $"\nSuggested hard cap: {profile.SuggestedHardStackCap.Value}"; } } return text2; } private string BuildProgressionPreview(ItemDef itemDef, int currentStack, CharacterBody body) { List list = new List { currentStack }; int num = currentStack; for (int i = 0; i < 4; i++) { ScalingPlan scalingPlan = _scaling.BuildPlan(itemDef, num, body); if (scalingPlan.TargetStack <= num) { break; } num = scalingPlan.TargetStack; list.Add(num); } string text = string.Join("→", list); if (list.Count > 6) { text = string.Join("→", list.GetRange(0, 5)) + "→..."; } return "\nNext pickups: " + text + ""; } private static string FormatFlags(UnsafeBehaviorFlags f) { if (f != 0) { return f.ToString(); } return "None"; } } [Flags] public enum UnsafeBehaviorFlags { None = 0, Movement = 1, ProcRecursion = 2, CooldownReduction = 4, DodgeOrInvulnerability = 8, VelocityScaling = 0x10, AttackSpeed = 0x20, ProjectileSpam = 0x40, AiBreaking = 0x80, PhysicsInstability = 0x100, DamageOverflow = 0x200, HealingRecursion = 0x400, InfiniteUptime = 0x800, MultiplayerDesyncRisk = 0x1000, ServerPerformanceRisk = 0x2000 } public enum UnsafeRiskTier { Safe, Mild, Unsafe, Extreme } public enum UnsafeCatalogSensitivity { ExtremeOnly, UnsafeAndAbove, MildAndAbove } public readonly struct BuiltinUnsafeItemProfile { public UnsafeRiskTier Tier { get; } public UnsafeBehaviorFlags Flags { get; } public ScalingMode RecommendedScaling { get; } public int? SuggestedHardStackCap { get; } public string Guidance { get; } public BuiltinUnsafeItemProfile(UnsafeRiskTier tier, UnsafeBehaviorFlags flags, ScalingMode recommendedScaling, int? suggestedHardStackCap, string guidance) { Tier = tier; Flags = flags; RecommendedScaling = recommendedScaling; SuggestedHardStackCap = suggestedHardStackCap; Guidance = guidance ?? string.Empty; } } internal static class UnsafeCatalogTierOrder { internal static int MinimumTierToTrigger(UnsafeCatalogSensitivity sensitivity) { return sensitivity switch { UnsafeCatalogSensitivity.ExtremeOnly => 3, UnsafeCatalogSensitivity.UnsafeAndAbove => 2, UnsafeCatalogSensitivity.MildAndAbove => 1, _ => 2, }; } internal static bool CatalogTierTriggersUnsafe(UnsafeRiskTier tier, UnsafeCatalogSensitivity sensitivity) { return (int)tier >= MinimumTierToTrigger(sensitivity); } } internal sealed class UnsafeItemRegistry { private readonly ExponentialItemsConfig _config; public UnsafeItemRegistry(ExponentialItemsConfig config) { _config = config; } public bool IsUnsafe(ItemDef itemDef) { if ((Object)(object)itemDef == (Object)null) { return false; } if (_config.IsManualExtraUnsafeItem(itemDef)) { return true; } if (_config.UseVanillaUnsafeCatalog.Value && VanillaUnsafeItemCatalog.TryGetProfile(itemDef, out var profile) && UnsafeCatalogTierOrder.CatalogTierTriggersUnsafe(profile.Tier, _config.RiskCatalogSensitivity.Value)) { return true; } if (!_config.UnsafeAutomaticLegacyPatterns.Value) { return false; } return MatchesLegacyNamePattern(((Object)itemDef).name); } public bool TryGetBuiltinProfile(ItemDef itemDef, out BuiltinUnsafeItemProfile profile) { return VanillaUnsafeItemCatalog.TryGetProfile(itemDef, out profile); } internal static bool MatchesLegacyNamePattern(string itemDefName) { if (string.IsNullOrEmpty(itemDefName)) { return false; } if (itemDefName.IndexOf("Hoof", StringComparison.OrdinalIgnoreCase) >= 0 || itemDefName.IndexOf("Feather", StringComparison.OrdinalIgnoreCase) >= 0 || itemDefName.IndexOf("Quail", StringComparison.OrdinalIgnoreCase) >= 0 || itemDefName.IndexOf("Syringe", StringComparison.OrdinalIgnoreCase) >= 0 || itemDefName.IndexOf("Times", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } return false; } } internal static class VanillaUnsafeItemCatalog { private static readonly Dictionary Profiles; public static int ProfileCount => Profiles.Count; static VanillaUnsafeItemCatalog() { Profiles = new Dictionary(StringComparer.OrdinalIgnoreCase); Reg("AutoCastEquipment", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.InfiniteUptime | UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 8, "Gesture of the Drowned: equipment + CDR feedback loops."); Reg("LunarBadLuck", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.Hyperbolic, 15, "Purity: warps luck and proc coefficients."); Reg("EquipmentMagazineVoid", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.InfiniteUptime | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 12, "Lysate Cell: extra equipment charges."); Reg("RandomEquipmentTrigger", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 8, "Bottled Chaos."); Reg("Clover", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 25, "57 Leaf Clover: combinatorial proc luck."); Reg("CloverVoid", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 8, "Benthic Bloom."); Reg("LunarSun", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.InfiniteUptime | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 12, "Egocentrism."); Reg("DroneWeapons", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProjectileSpam | UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 15, "Spare Drone Parts."); Reg("VoidMegaCrabItem", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 6, "Newly Hatched Zoea."); Reg("PhysicsProjectile", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.ProjectileSpam | UnsafeBehaviorFlags.PhysicsInstability | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 5, "Orphaned Core."); Reg("GhostOnKill", UnsafeRiskTier.Extreme, UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 12, "Happiest Mask."); Reg("Hoof", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 120, "Paul's Goat Hoof."); Reg("Feather", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.Hyperbolic, 40, "Hopoo Feather."); Reg("JumpBoost", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 80, "Wax Quail."); Reg("SprintBonus", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 100, "Energy Drink."); Reg("SprintArmor", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 80, "Rose Buckler."); Reg("SprintOutOfCombat", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 80, "Red Whip."); Reg("MoveSpeedOnKill", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 60, "Hunter's Harpoon."); Reg("Phasing", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.DodgeOrInvulnerability | UnsafeBehaviorFlags.VelocityScaling | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.Hyperbolic, 25, "Old War Stealthkit."); Reg("FallBoots", UnsafeRiskTier.Unsafe, Mv() | UnsafeBehaviorFlags.ProjectileSpam | UnsafeBehaviorFlags.PhysicsInstability, ScalingMode.Hyperbolic, 15, "H3AD-5T v2."); Reg("Mocha", UnsafeRiskTier.Unsafe, Mv() | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 80, "Mocha."); Reg("BoostAllStats", UnsafeRiskTier.Unsafe, Mv() | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 60, "Growth Nectar."); Reg("SpeedBoostPickup", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 80, "Elusive Antlers."); Reg("SpeedOnPickup", UnsafeRiskTier.Unsafe, Mv(), ScalingMode.SoftCap, 80, "Collector's Compulsion."); Reg("MushroomVoid", UnsafeRiskTier.Unsafe, Mv() | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 60, "Weeping Fungus."); Reg("WarCryOnMultiKill", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 80, "Berzerker's Pauldron."); Reg("WardOnLevel", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 40, "Warbanner."); Reg("JumpDamageStrike", UnsafeRiskTier.Unsafe, Mv() | UnsafeBehaviorFlags.ProjectileSpam, ScalingMode.SoftCap, 40, "Faraday Spur."); Reg("CritAtLowerElevation", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, 60, "Hiker's Boots."); Reg("TeleportOnLowHealth", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 30, "Unstable Transmitter."); Reg("KnockBackHitEnemies", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.PhysicsInstability | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 40, "Breaching Fin."); Reg("Syringe", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 100, "Soldier's Syringe."); Reg("AttackSpeedOnCrit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.AttackSpeed | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 60, "Predatory Instincts."); Reg("AttackSpeedPerNearbyAllyOrEnemy", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AttackSpeed | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 40, "Bolstering Lantern."); Reg("Bandolier", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.CooldownReduction, ScalingMode.SoftCap, 60, "Bandolier."); Reg("AlienHead", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 40, "Alien Head."); Reg("EquipmentMagazine", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.InfiniteUptime, ScalingMode.SoftCap, 25, "Fuel Cell."); Reg("Talisman", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.CooldownReduction, ScalingMode.SoftCap, 30, "Soulbound Catalyst."); Reg("KillEliteFrenzy", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.AttackSpeed | UnsafeBehaviorFlags.InfiniteUptime, ScalingMode.Hyperbolic, 25, "Brainstalks."); Reg("HalfAttackSpeedHalfCooldowns", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.AttackSpeed | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.Hyperbolic, 20, "Light Flux Pauldron."); Reg("HalfSpeedDoubleHealth", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.Hyperbolic, 15, "Stone Flux Pauldron."); Reg("EnergizedOnEquipmentUse", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 40, "War Horn."); Reg("UtilitySkillMagazine", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.ProjectileSpam, ScalingMode.SoftCap, 25, "Hardlight Afterburner."); Reg("Bear", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DodgeOrInvulnerability | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.Hyperbolic, 200, "Tougher Times."); Reg("BearVoid", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DodgeOrInvulnerability | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.Hyperbolic, 120, "Safer Spaces."); Reg("ChainLightning", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 60, "Ukulele."); Reg("ChainLightningVoid", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 50, "Polylute."); Reg("Missile", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 60, "AtG Missile Mk.1."); Reg("MissileVoid", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 50, "Plasma Shrimp."); Reg("MoreMissile", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 25, "Pocket I.C.B.M."); Reg("Dagger", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, 40, "Ceremonial Dagger."); Reg("Behemoth", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.PhysicsInstability, ScalingMode.SoftCap, 25, "Brilliant Behemoth."); Reg("FireballsOnHit", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 40, "Molten Perforator."); Reg("LightningStrikeOnHit", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 40, "Charged Perforator."); Reg("BounceNearby", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.PhysicsInstability, ScalingMode.SoftCap, 30, "Sentient Meat Hook."); Reg("StunAndPierce", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 40, "Electric Boomerang."); Reg("Icicle", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 35, "Frost Relic."); Reg("NovaOnHeal", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 35, "N'kuhana's Opinion."); Reg("ExplodeOnDeath", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 50, "Will-o'-the-wisp."); Reg("ExplodeOnDeathVoid", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 45, "Voidsent Flame."); Reg("IgniteOnKill", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 60, "Gasoline."); Reg("StrengthenBurn", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 50, "Ignition Tank."); Reg("StickyBomb", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 80, "Sticky Bomb."); Reg("ShockNearby", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 25, "Unstable Tesla Coil."); Reg("LaserTurbine", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 25, "Resonance Disc."); Reg("BarrageOnBoss", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 30, "War Bonds."); Reg("Firework", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 40, "Bundle of Fireworks."); Reg("Squid", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, 40, "Squid Polyp."); Reg("DronesDropDynamite", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.PhysicsInstability | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 15, "Box of Dynamite."); Reg("ElementalRingVoid", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 45, "Singularity Band."); Reg("PrimarySkillShuriken", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 45, "Shuriken."); Reg("BleedOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 80, "Tri-Tip Dagger."); Reg("BleedOnHitVoid", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 60, "Needletick."); Reg("DeathMark", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 60, "Death Mark."); Reg("Thorns", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 60, "Razorwire."); Reg("StunChanceOnHit", UnsafeRiskTier.Unsafe, Pv(), ScalingMode.SoftCap, 80, "Stun Grenade."); Reg("SlowOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 60, "Chronobauble."); Reg("SlowOnHitVoid", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 50, "Tentabauble."); Reg("TriggerEnemyDebuffs", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 35, "Noxious Thorn."); Reg("NovaOnLowHealth", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 25, "Genesis Loop."); Reg("HeadHunter", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, 25, "Wake of Vultures."); Reg("MeteorAttackOnHighDamage", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 20, "Runic Lens."); Reg("ShockDamageAura", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 20, "Faulty Conductor (item)."); Reg("SharedSuffering", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 25, "Networked Suffering."); Reg("BleedOnHitAndExplode", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.PhysicsInstability, ScalingMode.SoftCap, 25, "Shatterspleen."); Reg("SprintWisp", UnsafeRiskTier.Unsafe, Pv() | UnsafeBehaviorFlags.Movement, ScalingMode.SoftCap, 35, "Little Disciple."); Reg("WarCryOnCombat", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.AttackSpeed, ScalingMode.SoftCap, 50, "War Cry on combat."); Reg("DelayedDamage", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 30, "Warped Echo."); Reg("IncreasePrimaryDamage", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProjectileSpam | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 35, "Luminous Shot."); Reg("PermanentDebuffOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.Hyperbolic, 40, "Symbiotic Scorpion."); Reg("TransferDebuffOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.Hyperbolic, 20, "Neutronium Weight."); Reg("LunarDagger", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.Hyperbolic, 15, "Shaped Glass."); Reg("ArmorReductionOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 30, "Shattering Justice."); Reg("IncreaseDamageOnMultiKill", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, 40, "Chronic Expansion."); Reg("RepeatHeal", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.InfiniteUptime, ScalingMode.Hyperbolic, 15, "Corpsebloom."); Reg("BarrierOnOverHeal", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 40, "Aegis."); Reg("IncreaseHealing", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 40, "Rejuvenation Rack."); Reg("HealOnCrit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AttackSpeed | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, 50, "Harvester's Scythe."); Reg("ExtraLife", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.InfiniteUptime, ScalingMode.Hyperbolic, 10, "Dio's Best Friend."); Reg("ExtraLifeVoid", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.InfiniteUptime, ScalingMode.Hyperbolic, 10, "Pluripotent Larva."); Reg("CaptainDefenseMatrix", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProjectileSpam | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 20, "Defensive Microbots."); Reg("RoboBallBuddy", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 20, "Empathy Cores."); Reg("MinorConstructOnKill", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, 25, "Defense Nucleus."); Reg("BeetleGland", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, 30, "Queen's Gland."); Reg("MonstersOnShrineUse", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, 25, "Defiant Gouge."); Reg("RandomDamageZone", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 20, "Mercurial Rachis."); Reg("GoldOnHit", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 25, "Brittle Crown."); Reg("GoldOnHurt", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, 40, "Roll of Pennies."); Reg("FocusConvergence", UnsafeRiskTier.Unsafe, UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, 15, "Focused Convergence."); Reg("CritGlasses", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, null, "Lens-Maker's Glasses: crit chance saturates; soft-cap recommended."); Reg("CritGlassesVoid", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, null, "Lost Seer's Lenses."); Reg("CritDamage", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Laser Scope."); Reg("Crowbar", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Crowbar: damage vs high-health."); Reg("NearbyDamageBonus", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Focus Crystal."); Reg("BossDamageBonus", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Armor-Piercing Rounds."); Reg("SecondarySkillMagazine", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, null, "Backup Magazine — mild proc volume increase."); Reg("FlatHealth", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Bison Steak."); Reg("Medkit", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Medkit."); Reg("Tooth", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Monster Tooth."); Reg("Seed", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Leeching Seed."); Reg("Mushroom", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Bustling Fungus."); Reg("BarrierOnKill", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Topaz Brooch."); Reg("PersonalShield", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Personal Shield Generator."); Reg("ArmorPlate", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Repulsion Armor Plate."); Reg("ExecuteLowHealthElite", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.AiBreaking, ScalingMode.SoftCap, null, "Old Guillotine."); Reg("TPHealingNova", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Lepton Daisy."); Reg("BarrierOnCooldown", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.CooldownReduction | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Eclipse Lite."); Reg("Infusion", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Infusion."); Reg("ShieldBooster", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Kinetic Dampener."); Reg("ImmuneToDebuff", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DodgeOrInvulnerability, ScalingMode.SoftCap, null, "Ben's Raincoat."); Reg("OutOfCombatArmor", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Oddly-shaped Opal."); Reg("FragileDamageBonus", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Delicate Watch."); Reg("BonusGoldPackOnKill", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Ghor's Tome."); Reg("TreasureCache", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Rusted Key."); Reg("TreasureCacheVoid", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Encrusted Key."); Reg("FreeChest", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Shipping Request Form."); Reg("RegeneratingScrap", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Regenerating Scrap."); Reg("LowerPricedChests", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Sale Star."); Reg("ItemDropChanceOnKill", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Sonorous Whispers."); Reg("Duplicator", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.MultiplayerDesyncRisk | UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Substandard Duplicator."); Reg("Plant", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.AiBreaking | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Interstellar Desk Plant."); Reg("Knurl", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Titanic Knurl."); Reg("Pearl", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Pearl."); Reg("ShinyPearl", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Irradiant Pearl."); Reg("SiphonOnLowHealth", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Mired Urn."); Reg("ParentEgg", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Planula."); Reg("HealingPotion", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Power Elixir."); Reg("HealWhileSafe", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.HealingRecursion, ScalingMode.SoftCap, null, "Cautious Slug."); Reg("AdaptiveArmor", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Adaptive armor utility item."); Reg("ShieldOnly", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow | UnsafeBehaviorFlags.HealingRecursion, ScalingMode.Hyperbolic, null, "Transcendence."); Reg("TonicAffliction", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Tonic Affliction."); Reg("OnLevelUpFreeUnlock", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Longstanding Solitude."); Reg("ExtraShrineItem", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Chance Doll."); Reg("ExtraStatsOnLevelUp", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Prayer Beads."); Reg("LunarTrinket", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.MultiplayerDesyncRisk, ScalingMode.SoftCap, null, "Beads of Fealty."); Reg("LunarUtilityReplacement", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.CooldownReduction, ScalingMode.SoftCap, null, "Strides of Heresy."); Reg("LunarPrimaryReplacement", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProjectileSpam, ScalingMode.SoftCap, null, "Visions of Heresy."); Reg("LunarSecondaryReplacement", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProjectileSpam, ScalingMode.SoftCap, null, "Hooks of Heresy."); Reg("LunarSpecialReplacement", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProjectileSpam, ScalingMode.SoftCap, null, "Essence of Heresy."); Reg("ConvertCritChanceToCritDamage", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.DamageOverflow, ScalingMode.SoftCap, null, "Crit-to-damage conversion (SotV)."); Reg("RandomlyLunar", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ServerPerformanceRisk, ScalingMode.SoftCap, null, "Eulogy Zero."); Reg("EmpowerAlways", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.ProcRecursion, ScalingMode.SoftCap, null, "Empower Always."); Reg("IceRing", UnsafeRiskTier.Mild, Pv(), ScalingMode.SoftCap, null, "Runald's Band."); Reg("FireRing", UnsafeRiskTier.Mild, Pv(), ScalingMode.SoftCap, null, "Kjaro's Band."); Reg("LunarWings", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.Movement, ScalingMode.SoftCap, null, "Blessings of Terafirmae."); Reg("ExtraEquipment", UnsafeRiskTier.Mild, UnsafeBehaviorFlags.CooldownReduction, ScalingMode.SoftCap, null, "Functional Coupler."); } private static UnsafeBehaviorFlags Mv() { return UnsafeBehaviorFlags.Movement | UnsafeBehaviorFlags.VelocityScaling; } private static UnsafeBehaviorFlags Pv() { return UnsafeBehaviorFlags.ProcRecursion | UnsafeBehaviorFlags.ProjectileSpam; } private static void Reg(string name, UnsafeRiskTier tier, UnsafeBehaviorFlags flags, ScalingMode recommended, int? hardCap, string guidance) { if (!string.IsNullOrEmpty(name)) { Profiles[name] = new BuiltinUnsafeItemProfile(tier, flags, recommended, hardCap, guidance); } } public static bool TryGetProfileByName(string itemDefName, out BuiltinUnsafeItemProfile profile) { if (string.IsNullOrEmpty(itemDefName)) { profile = default(BuiltinUnsafeItemProfile); return false; } return Profiles.TryGetValue(itemDefName, out profile); } public static bool TryGetProfile(ItemDef def, out BuiltinUnsafeItemProfile profile) { profile = default(BuiltinUnsafeItemProfile); if ((Object)(object)def != (Object)null) { return TryGetProfileByName(((Object)def).name, out profile); } return false; } } }