Decompiled source of RoleShuffle v5.0.0

RoleShuffle.dll

Decompiled 4 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using MenuLib;
using MenuLib.MonoBehaviors;
using MenuLib.Structs;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.Localization;
using UnityEngine.SceneManagement;
using UnityEngine.TextCore;
using UnityEngine.TextCore.LowLevel;
using UnityEngine.UI;

[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("Codex")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("5.0.0.0")]
[assembly: AssemblyInformationalVersion("5.0.0+0ff3d50b66e8bf663340c6d9be2331492e6add92")]
[assembly: AssemblyProduct("RoleShuffle")]
[assembly: AssemblyTitle("RoleShuffle")]
[assembly: AssemblyVersion("5.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

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

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace REPOJP.StageRoles
{
	[BepInPlugin("REPOJP.RoleShuffle", "RoleShuffle", "5.0.0")]
	[BepInDependency("nickklmao.repoconfig", "1.2.7")]
	[BepInDependency("nickklmao.menulib", "2.6.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class StageRolesPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "REPOJP.RoleShuffle";

		public const string PluginName = "RoleShuffle";

		public const string PluginVersion = "5.0.0";

		public const string StageFluxGuid = "REPOJP.StagePhysicsEvents";

		public const string RepoConfigGuid = "nickklmao.repoconfig";

		public const string MenuLibGuid = "nickklmao.menulib";

		private Harmony? _harmony;

		private GameObject? _controllerObject;

		private object? _guidePublishedRoom;

		private string _publishedBaseUpgradeSignature = string.Empty;

		private float _nextBaseUpgradePublishAt;

		private bool _settingsPublishPending;

		private float _settingsPublishAt;

		private float _nextPrivacyRefresh;

		internal RoleBugReport BugReport { get; private set; }

		internal static StageRolesPlugin Instance { get; private set; }

		internal static ManualLogSource ModLogger { get; private set; }

		internal StageRolesConfig Settings { get; private set; }

		internal RoleSelectionSettings RoleSettings { get; private set; }

		internal RoleTestMenuService RoleTests { get; private set; }

		internal BaseUpgradeSelectionSettings BaseUpgradeSettings { get; private set; }

		internal StageRoleController Controller { get; private set; }

		internal NegotiatorRuntime Negotiator { get; private set; }

		internal BaseUpgradeDrawRuntime BaseUpgradeDraw { get; private set; }

		internal void SaveLocalSettings()
		{
			((BaseUnityPlugin)this).Config.Save();
		}

		internal void CreateBugReport()
		{
			BugReport.Create(((BaseUnityPlugin)this).Config);
		}

		private void Awake()
		{
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Expected O, but got Unknown
			//IL_01c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cf: Expected O, but got Unknown
			Instance = this;
			ModLogger = ((BaseUnityPlugin)this).Logger;
			BugReport = new RoleBugReport();
			Logger.Listeners.Add((ILogListener)(object)BugReport);
			Settings = new StageRolesConfig(((BaseUnityPlugin)this).Config);
			RoleSettings = new RoleSelectionSettings(Settings, ((BaseUnityPlugin)this).Config);
			BaseUpgradeSettings = new BaseUpgradeSelectionSettings(Settings, ((BaseUnityPlugin)this).Config);
			((BaseUnityPlugin)this).Config.SettingChanged += ConfigSettingChanged;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
			_controllerObject = new GameObject("RoleShuffle_Controller");
			((Object)_controllerObject).hideFlags = (HideFlags)61;
			_controllerObject.transform.SetParent(((Component)this).transform, false);
			Controller = _controllerObject.AddComponent<StageRoleController>();
			Controller.Initialize(Settings);
			RoleTests = new RoleTestMenuService(Settings, Controller);
			Negotiator = ((Component)this).gameObject.AddComponent<NegotiatorRuntime>();
			Negotiator.Initialize(Settings);
			((Component)this).gameObject.AddComponent<RoleHud>().Initialize(Settings);
			((Component)this).gameObject.AddComponent<RoleResourceHud>().Initialize(Settings);
			((Component)this).gameObject.AddComponent<RoleMenu>().Initialize(Settings);
			((Component)this).gameObject.AddComponent<RoleSyncStatus>().Initialize(Settings);
			BaseUpgradeDraw = ((Component)this).gameObject.AddComponent<BaseUpgradeDrawRuntime>();
			BaseUpgradeDraw.Initialize(Settings);
			((Component)this).gameObject.AddComponent<RoleTestCommandService>().Initialize(Settings, Controller);
			_harmony = new Harmony("REPOJP.RoleShuffle");
			try
			{
				_harmony.PatchAll(typeof(LifecyclePatches));
				_harmony.PatchAll(typeof(TwinsPatches));
				_harmony.PatchAll(typeof(TwinsHealthUpgradePatch));
				_harmony.PatchAll(typeof(RoleExpressionPatches));
				_harmony.PatchAll(typeof(NinjaVisionPatch));
				_harmony.PatchAll(typeof(NinjaVoiceInvestigationPatch));
				_harmony.PatchAll(typeof(NinjaLandingInvestigationPatch));
				_harmony.PatchAll(typeof(NinjaFootstepInvestigationPatch));
				_harmony.PatchAll(typeof(InfluencerFootstepInvestigationPatch));
				_harmony.PatchAll(typeof(InfluencerLandingInvestigationPatch));
				_harmony.PatchAll(typeof(InfluencerVoiceInvestigationPatch));
				_harmony.PatchAll(typeof(InfluencerInvestigationRadiusPatch));
				_harmony.PatchAll(typeof(EngineerTrapUpdatePatches));
				_harmony.PatchAll(typeof(EngineerEffectActivationPatches));
				_harmony.PatchAll(typeof(EngineerModernFramePatch));
				_harmony.PatchAll(typeof(EngineerModernActionPatch));
				_harmony.PatchAll(typeof(EngineerModernConditionPatch));
				_harmony.PatchAll(typeof(EngineerGolfcartDriverPatch));
				_harmony.PatchAll(typeof(EngineerInstrumentNoisePatch));
				_harmony.PatchAll(typeof(ShopPatches));
				_harmony.PatchAll(typeof(UpgradeItemRetentionPatch));
				_harmony.PatchAll(typeof(NotificationEnemyReactionPatches));
				_harmony.PatchAll(typeof(HunterBatteryPatches));
				_harmony.PatchAll(typeof(DualWielderPatches));
				_harmony.PatchAll(typeof(RiggerPatches));
				_harmony.PatchAll(typeof(NegotiatorPatches));
				_harmony.PatchAll(typeof(MusicianPatches));
				_harmony.PatchAll(typeof(MageStaffProjectilePatch));
				_harmony.PatchAll(typeof(MageBeamCarrierStartPatch));
				_harmony.PatchAll(typeof(MageBeamCarrierAimPatch));
				_harmony.PatchAll(typeof(MageBeamCarrierRecoilPatch));
				_harmony.PatchAll(typeof(MageStaffImpactPatch));
				_harmony.PatchAll(typeof(MageStaffVoidDurationPatch));
				_harmony.PatchAll(typeof(MageStaffBeamDurationPatch));
				_harmony.PatchAll(typeof(RoleGuideScrollPatch));
				LifterStrengthPatch.Install(_harmony);
				SceneManager.activeSceneChanged += ActiveSceneChanged;
				((BaseUnityPlugin)this).Logger.LogInfo((object)"RoleShuffle 5.0.0 loaded.");
			}
			catch (Exception arg)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)$"Failed to apply Harmony patches: {arg}");
				_harmony.UnpatchSelf();
				((Behaviour)Controller).enabled = false;
			}
		}

		private void ActiveSceneChanged(Scene previous, Scene current)
		{
			RoleHudTestPreview.Clear();
			RoleHudEditor.Instance?.Close(save: false);
			BaseUpgradeHistory.ClearDisplayPreview();
			Controller?.StageEnding();
			RoleDrawEligibilitySync.Invalidate();
			Negotiator?.SceneChanged();
		}

		private void Update()
		{
			if (Time.unscaledTime >= _nextPrivacyRefresh)
			{
				_nextPrivacyRefresh = Time.unscaledTime + 1f;
				BugReport.RememberPlayers();
			}
			if ((Object)(object)GameManager.instance == (Object)null)
			{
				return;
			}
			RoleDrawEligibilitySync.Tick(Controller);
			if (_settingsPublishPending && Time.unscaledTime >= _settingsPublishAt)
			{
				_settingsPublishPending = false;
				RoleGuideSync.Publish(Settings);
				BaseUpgradeSync.Publish(Settings);
				_publishedBaseUpgradeSignature = BaseUpgradeSync.LocalPublishSignature(Settings);
			}
			object currentRoom = PhotonNetwork.CurrentRoom;
			if (!SemiFunc.IsMultiplayer() || !PhotonNetwork.IsMasterClient || currentRoom == null)
			{
				_guidePublishedRoom = null;
				_publishedBaseUpgradeSignature = string.Empty;
				return;
			}
			if (_guidePublishedRoom != currentRoom)
			{
				RoleGuideSync.Publish(Settings);
				BaseUpgradeSync.Publish(Settings);
				_guidePublishedRoom = currentRoom;
				_publishedBaseUpgradeSignature = BaseUpgradeSync.LocalPublishSignature(Settings);
				_nextBaseUpgradePublishAt = Time.unscaledTime + 0.5f;
			}
			if (!(Time.unscaledTime < _nextBaseUpgradePublishAt))
			{
				_nextBaseUpgradePublishAt = Time.unscaledTime + 0.5f;
				string text = BaseUpgradeSync.LocalPublishSignature(Settings);
				if (!string.Equals(text, _publishedBaseUpgradeSignature, StringComparison.Ordinal))
				{
					BaseUpgradeSync.Publish(Settings);
					_publishedBaseUpgradeSignature = text;
				}
			}
		}

		private void ConfigSettingChanged(object sender, SettingChangedEventArgs args)
		{
			RoleDrawEligibilitySync.Invalidate();
			if (!Settings.SetRoleCommandEnabled.Value)
			{
				BaseUpgradeHistory.ClearDisplayPreview();
				RoleHudTestPreview.Clear();
			}
			string section = args.ChangedSetting.Definition.Section;
			if ((!(section == "HUD") && !(section == "UI")) || 1 == 0)
			{
				RoleGuideSync.Invalidate();
				_settingsPublishPending = true;
				_settingsPublishAt = Time.unscaledTime + 0.15f;
			}
		}

		private void OnDestroy()
		{
			RoleHudTestPreview.Clear();
			RoleHudEditor.Instance?.Close(save: false);
			BaseUpgradeHistory.ClearDisplayPreview();
			Logger.Listeners.Remove((ILogListener)(object)BugReport);
			BugReport?.Dispose();
			((BaseUnityPlugin)this).Config.SettingChanged -= ConfigSettingChanged;
			SceneManager.activeSceneChanged -= ActiveSceneChanged;
			Controller?.Shutdown();
			BaseUpgradeDraw?.Shutdown();
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			StageFluxCompatibility.ClearNotifications();
			RoleGuideSync.Invalidate();
			RoleAssignmentSync.ResetCache();
			RoleEmblems.Shutdown();
			if ((Object)(object)_controllerObject != (Object)null)
			{
				Object.Destroy((Object)(object)_controllerObject);
			}
		}
	}
	internal enum UpgradeItemScope
	{
		Player,
		AllPlayers
	}
	internal sealed class StageRolesConfig
	{
		internal const int MaximumSupportedPlayers = 30;

		private readonly Dictionary<string, ConfigEntry<int>> _truckUpgradeDrawWeights = new Dictionary<string, ConfigEntry<int>>(StringComparer.Ordinal);

		private readonly Dictionary<string, ConfigEntry<bool>> _baseUpgradeDrawEnabled = new Dictionary<string, ConfigEntry<bool>>(StringComparer.Ordinal);

		internal ConfigEntry<bool> TwinsEnabled { get; }

		internal ConfigEntry<bool> NegotiatorEnabled { get; }

		internal ConfigEntry<int> NegotiatorWeight { get; }

		internal ConfigEntry<float> NegotiatorDeliveryCarryDistance { get; }

		internal ConfigEntry<string> NegotiatorDeliveryAllowedSizes { get; }

		internal ConfigEntry<int> NegotiatorEnemyTier { get; }

		internal ConfigEntry<float> RiggerEnemyDamageMultiplier { get; }

		internal ConfigEntry<bool> RiggerEnabled { get; }

		internal ConfigEntry<int> RiggerWeight { get; }

		internal ConfigEntry<float> RiggerEnemyHoldMultiplier { get; }

		internal ConfigEntry<float> RiggerDurabilityMultiplier { get; }

		internal ConfigEntry<bool> DualWielderEnabled { get; }

		internal ConfigEntry<int> DualWielderWeight { get; }

		internal ConfigEntry<float> DualWielderDelaySeconds { get; }

		internal ConfigEntry<float> DualWielderLeftOffset { get; }

		internal ConfigEntry<bool> PorterEnabled { get; }

		internal ConfigEntry<int> PorterWeight { get; }

		internal ConfigEntry<float> PorterCapacity { get; }

		internal ConfigEntry<float> PorterHoldSeconds { get; }

		internal ConfigEntry<float> PorterUnloadSeconds { get; }

		internal ConfigEntry<bool> PorterDropOnEnemyHit { get; }

		internal ConfigEntry<string> PorterAllowedSizes { get; }

		internal ConfigEntry<int> TwinsWeight { get; }

		internal ConfigEntry<float> TwinsCarryStrengthMultiplier { get; }

		internal ConfigEntry<float> TwinsCollisionReductionPercent { get; }

		internal ConfigEntry<float> TwinsRestRadius { get; }

		internal ConfigEntry<float> TwinsRestHoldSeconds { get; }

		internal ConfigEntry<float> TwinsRestHealPercent { get; }

		internal ConfigEntry<float> TwinsRestCooldownSeconds { get; }

		internal ConfigEntry<float> TwinsRendezvousStartRange { get; }

		internal ConfigEntry<float> TwinsRendezvousEndRange { get; }

		internal ConfigEntry<float> TwinsRendezvousSpeedMultiplier { get; }

		internal ConfigEntry<float> TwinsDeliveryCarrySeconds { get; }

		internal ConfigEntry<float> TwinsDeliveryRadius { get; }

		internal ConfigEntry<float> TwinsDeliveryBonusPercent { get; }

		internal ConfigEntry<int> TwinsDeliveryStageLimit { get; }

		internal ConfigEntry<bool> Enabled { get; }

		internal ConfigEntry<bool> UniqueRoles { get; }

		internal ConfigEntry<bool> HudResourcesEnabled { get; }

		internal ConfigEntry<int> HudResourceScale { get; }

		internal ConfigEntry<int> HudResourceOffsetX { get; }

		internal ConfigEntry<int> HudResourceOffsetY { get; }

		internal ConfigEntry<float> TankHealthMultiplier { get; }

		internal ConfigEntry<float> RunnerSpeedMultiplier { get; }

		internal ConfigEntry<float> RunnerStaminaMultiplier { get; }

		internal ConfigEntry<int> TankMaximumHealth { get; }

		internal ConfigEntry<int> RunnerMaximumSpeed { get; }

		internal ConfigEntry<int> RunnerMaximumStamina { get; }

		internal ConfigEntry<float> JoblessContractDistance { get; }

		internal ConfigEntry<float> JoblessInitialGrace { get; }

		internal ConfigEntry<float> JoblessTinyGrace { get; }

		internal ConfigEntry<float> JoblessSmallGrace { get; }

		internal ConfigEntry<float> JoblessMediumGrace { get; }

		internal ConfigEntry<float> JoblessBigGrace { get; }

		internal ConfigEntry<float> JoblessWideGrace { get; }

		internal ConfigEntry<float> JoblessTallGrace { get; }

		internal ConfigEntry<float> JoblessVeryTallGrace { get; }

		internal ConfigEntry<int> JoblessTinyHeal { get; }

		internal ConfigEntry<int> JoblessSmallHeal { get; }

		internal ConfigEntry<int> JoblessMediumHeal { get; }

		internal ConfigEntry<int> JoblessBigHeal { get; }

		internal ConfigEntry<int> JoblessWideHeal { get; }

		internal ConfigEntry<int> JoblessTallHeal { get; }

		internal ConfigEntry<int> JoblessVeryTallHeal { get; }

		internal ConfigEntry<float> KingUpgradeRadius { get; }

		internal ConfigEntry<int> KingSpeedBonus { get; }

		internal ConfigEntry<int> KingRangeBonus { get; }

		internal ConfigEntry<int> KingStrengthBonus { get; }

		internal ConfigEntry<int> ShopUpgradeItemCount { get; }

		internal ConfigEntry<bool> EnhancedEnemyRewardsEnabled { get; }

		internal ConfigEntry<bool> ShowcaseGuaranteesEnabled { get; }

		internal ConfigEntry<string> ShowcaseMinimumsByPlayerCount { get; }

		internal ConfigEntry<bool> SupportGuaranteesEnabled { get; }

		internal ConfigEntry<string> SupportMinimumsByPlayerCount { get; }

		internal ConfigEntry<bool> RiskCombinationLimitsEnabled { get; }

		internal ConfigEntry<string> DangerRoleLimit { get; }

		internal ConfigEntry<string> HardshipRoleLimit { get; }

		internal ConfigEntry<int> SmallPartyCombinedRiskPlayerCount { get; }

		internal ConfigEntry<int> SmallPartyCombinedRiskRoleLimit { get; }

		internal ConfigEntry<bool> PreventConsecutiveSameRole { get; }

		internal ConfigEntry<int> HardshipPersonalCooldownStages { get; }

		internal ConfigEntry<bool> ExcludeUnavailableContextRoles { get; }

		internal ConfigEntry<bool> AnnouncementsEnabled { get; }

		internal ConfigEntry<float> AnnouncementDelaySeconds { get; }

		internal ConfigEntry<float> StageFluxAnnouncementDelaySeconds { get; }

		internal ConfigEntry<int> HudFontSize { get; }

		internal ConfigEntry<bool> HudEnabled { get; }

		internal ConfigEntry<string> HudRoleDisplay { get; }

		internal ConfigEntry<int> HudIconSize { get; }

		internal ConfigEntry<string> HudAnchor { get; }

		internal ConfigEntry<string> HudAlignment { get; }

		internal ConfigEntry<int> HudOffsetX { get; }

		internal ConfigEntry<int> HudOffsetY { get; }

		internal ConfigEntry<int> HudScalePercent { get; }

		internal ConfigEntry<int> HudPlayersPerPage { get; }

		internal ConfigEntry<float> HudPageIntervalSeconds { get; }

		internal ConfigEntry<float> HudTransitionDurationSeconds { get; }

		internal ConfigEntry<bool> HudPinLocalPlayer { get; }

		internal ConfigEntry<bool> SetRoleCommandEnabled { get; }

		internal ConfigEntry<bool> KeepUpgradeItems { get; }

		internal ConfigEntry<UpgradeItemScope> UpgradeItemScope { get; }

		internal ConfigEntry<bool> BaseUpgradeManualAdjustmentEnabled { get; }

		internal ConfigEntry<string> BaseHealthLevels { get; }

		internal ConfigEntry<string> BaseStaminaLevels { get; }

		internal ConfigEntry<string> BaseExtraJumpLevels { get; }

		internal ConfigEntry<string> BaseSpeedLevels { get; }

		internal ConfigEntry<string> BaseStrengthLevels { get; }

		internal ConfigEntry<string> BaseRangeLevels { get; }

		internal ConfigEntry<string> BaseLaunchLevels { get; }

		internal ConfigEntry<string> BaseTumbleClimbLevels { get; }

		internal ConfigEntry<string> BaseTumbleWingsLevels { get; }

		internal ConfigEntry<string> BaseCrouchRestLevels { get; }

		internal ConfigEntry<string> BaseMapPlayerCountLevels { get; }

		internal ConfigEntry<string> BaseDeathHeadBatteryLevels { get; }

		internal ConfigEntry<bool> TruckUpgradeDrawEnabled { get; }

		internal ConfigEntry<int> TruckUpgradeDrawMaximumLevel { get; }

		internal ConfigEntry<float> TruckUpgradeDrawCappedWeightMultiplier { get; }

		internal ConfigEntry<float> TruckUpgradeDrawWeightFalloffExponent { get; }

		internal ConfigEntry<string> TruckUpgradeDrawDeltaWeights { get; }

		internal ConfigEntry<bool> TankEnabled { get; }

		internal ConfigEntry<int> TankWeight { get; }

		internal ConfigEntry<int> TankHealthLevels { get; }

		internal ConfigEntry<bool> RunnerEnabled { get; }

		internal ConfigEntry<int> RunnerWeight { get; }

		internal ConfigEntry<int> RunnerSpeedLevels { get; }

		internal ConfigEntry<int> RunnerStaminaLevels { get; }

		internal ConfigEntry<bool> JumperEnabled { get; }

		internal ConfigEntry<int> JumperWeight { get; }

		internal ConfigEntry<int> JumperExtraJumpLevels { get; }

		internal ConfigEntry<bool> LifterEnabled { get; }

		internal ConfigEntry<int> LifterWeight { get; }

		internal ConfigEntry<int> LifterStrengthLevel { get; }

		internal ConfigEntry<int> LifterLightItemStrengthLevel { get; }

		internal ConfigEntry<bool> LauncherEnabled { get; }

		internal ConfigEntry<int> LauncherWeight { get; }

		internal ConfigEntry<int> LauncherLaunchLevels { get; }

		internal ConfigEntry<bool> ClimberEnabled { get; }

		internal ConfigEntry<int> ClimberWeight { get; }

		internal ConfigEntry<int> ClimberClimbLevels { get; }

		internal ConfigEntry<int> ClimberRangeLevels { get; }

		internal ConfigEntry<bool> FlyerEnabled { get; }

		internal ConfigEntry<int> FlyerWeight { get; }

		internal ConfigEntry<int> FlyerWingsLevels { get; }

		internal ConfigEntry<bool> TrackerEnabled { get; }

		internal ConfigEntry<int> TrackerWeight { get; }

		internal ConfigEntry<int> TrackerHealthLevels { get; }

		internal ConfigEntry<float> TrackerEnemyRange { get; }

		internal ConfigEntry<float> TrackerDangerRange { get; }

		internal ConfigEntry<float> TrackerNotificationInterval { get; }

		internal ConfigEntry<float> TrackerDistanceChange { get; }

		internal ConfigEntry<bool> GhostEnabled { get; }

		internal ConfigEntry<int> GhostWeight { get; }

		internal ConfigEntry<int> GhostDeathHeadBatteryLevels { get; }

		internal ConfigEntry<float> GhostHealRadius { get; }

		internal ConfigEntry<int> GhostHealAmount { get; }

		internal ConfigEntry<float> GhostHealInterval { get; }

		internal ConfigEntry<int> GhostTotalHealingLimit { get; }

		internal ConfigEntry<int> GhostCarrierSpeedBonus { get; }

		internal ConfigEntry<bool> BomberEnabled { get; }

		internal ConfigEntry<int> BomberWeight { get; }

		internal ConfigEntry<float> BomberDistance { get; }

		internal ConfigEntry<int> BomberMaximumActiveGrenades { get; }

		internal ConfigEntry<bool> BomberAllowTruckSpawns { get; }

		internal ConfigEntry<bool> BomberExplosiveEnabled { get; }

		internal ConfigEntry<bool> BomberStunEnabled { get; }

		internal ConfigEntry<bool> BomberShockwaveEnabled { get; }

		internal ConfigEntry<bool> BomberDuctTapedEnabled { get; }

		internal ConfigEntry<bool> MedicEnabled { get; }

		internal ConfigEntry<int> MedicWeight { get; }

		internal ConfigEntry<int> MedicHealAmount { get; }

		internal ConfigEntry<float> MedicHealIntervalSeconds { get; }

		internal ConfigEntry<float> MedicHealRadius { get; }

		internal ConfigEntry<int> MedicTotalHealingLimit { get; }

		internal ConfigEntry<bool> PhoenixEnabled { get; }

		internal ConfigEntry<int> PhoenixWeight { get; }

		internal ConfigEntry<int> PhoenixRevivalHealth { get; }

		internal ConfigEntry<float> PhoenixReviveDelaySeconds { get; }

		internal ConfigEntry<bool> JoblessEnabled { get; }

		internal ConfigEntry<int> JoblessWeight { get; }

		internal ConfigEntry<int> JoblessDamage { get; }

		internal ConfigEntry<float> JoblessDamageIntervalSeconds { get; }

		internal ConfigEntry<bool> RescuerEnabled { get; }

		internal ConfigEntry<int> RescuerWeight { get; }

		internal ConfigEntry<int> RescuerRevivalHealth { get; }

		internal ConfigEntry<float> RescuerReviveDelaySeconds { get; }

		internal ConfigEntry<int> RescuerMaximumRevives { get; }

		internal ConfigEntry<bool> VampireEnabled { get; }

		internal ConfigEntry<int> VampireWeight { get; }

		internal ConfigEntry<int> VampireTier1HealAmount { get; }

		internal ConfigEntry<int> VampireTier2HealAmount { get; }

		internal ConfigEntry<int> VampireTier3HealAmount { get; }

		internal ConfigEntry<float> VampireRadius { get; }

		internal ConfigEntry<bool> KingEnabled { get; }

		internal ConfigEntry<int> KingWeight { get; }

		internal ConfigEntry<bool> TunaEnabled { get; }

		internal ConfigEntry<int> TunaWeight { get; }

		internal ConfigEntry<float> TunaStationaryDelaySeconds { get; }

		internal ConfigEntry<int> TunaDamage { get; }

		internal ConfigEntry<float> TunaDamageIntervalSeconds { get; }

		internal ConfigEntry<bool> MusicianEnabled { get; }

		internal ConfigEntry<int> MusicianWeight { get; }

		internal ConfigEntry<int> MusicianHealAmount { get; }

		internal ConfigEntry<float> MusicianHealRadius { get; }

		internal ConfigEntry<float> MusicianHealCooldownSeconds { get; }

		internal ConfigEntry<int> MusicianTotalHealingLimit { get; }

		internal ConfigEntry<bool> MageEnabled { get; }

		internal ConfigEntry<int> MageWeight { get; }

		internal ConfigEntry<float> MageCastIntervalSeconds { get; }

		internal ConfigEntry<float> MageBeamDurationSeconds { get; }

		internal ConfigEntry<string> MageStarExpression { get; }

		internal ConfigEntry<string> MageRollExpression { get; }

		internal ConfigEntry<string> MageGravityExpression { get; }

		internal ConfigEntry<string> MageVoidExpression { get; }

		internal ConfigEntry<string> MageLaserExpression { get; }

		internal ConfigEntry<bool> MageAutoRecoveryEnabled { get; }

		internal ConfigEntry<float> MageAutoRecoveryDelaySeconds { get; }

		internal ConfigEntry<float> MageAutoRecoveryIntervalSeconds { get; }

		internal ConfigEntry<int> MageAutoRecoveryAmount { get; }

		internal ConfigEntry<int> MageAutoRecoveryTotalHealingLimit { get; }

		internal ConfigEntry<int> MageStarHealthCost { get; }

		internal ConfigEntry<int> MageGravityHealthCost { get; }

		internal ConfigEntry<int> MageRollHealthCost { get; }

		internal ConfigEntry<int> MageVoidHealthCost { get; }

		internal ConfigEntry<int> MageLaserHealthCost { get; }

		internal ConfigEntry<bool> GamblerEnabled { get; }

		internal ConfigEntry<int> GamblerWeight { get; }

		internal ConfigEntry<int> GamblerWinChancePercent { get; }

		internal ConfigEntry<float> GamblerWinValueMultiplier { get; }

		internal ConfigEntry<int> GamblerGambitGreenHealAmount { get; }

		internal ConfigEntry<int> GamblerGambitRedDamage { get; }

		internal ConfigEntry<int> GamblerGambitWhiteHealthUpgradeLevels { get; }

		internal ConfigEntry<bool> HunterEnabled { get; }

		internal ConfigEntry<int> HunterWeight { get; }

		internal ConfigEntry<int> HunterBatteryConsumptionPercent { get; }

		internal ConfigEntry<float> HunterDoubleOrbChancePercent { get; }

		internal ConfigEntry<float> HunterJackpotOrbChancePercent { get; }

		internal ConfigEntry<int> HunterJackpotOrbCount { get; }

		internal ConfigEntry<int> HunterGuaranteedOrbEveryKills { get; }

		internal ConfigEntry<bool> StinkerEnabled { get; }

		internal ConfigEntry<int> StinkerWeight { get; }

		internal ConfigEntry<float> StinkerDistance { get; }

		internal ConfigEntry<float> StinkerSafetyDistance { get; }

		internal ConfigEntry<bool> StinkerAllowTruckSpawns { get; }

		internal ConfigEntry<float> StinkerBreakGraceSeconds { get; }

		internal ConfigEntry<bool> EngineerEnabled { get; }

		internal ConfigEntry<int> EngineerWeight { get; }

		internal ConfigEntry<bool> TricksterEnabled { get; }

		internal ConfigEntry<int> TricksterWeight { get; }

		internal ConfigEntry<string> TricksterExpression { get; }

		internal ConfigEntry<float> TricksterActiveSeconds { get; }

		internal ConfigEntry<float> TricksterCooldownSeconds { get; }

		internal ConfigEntry<float> TricksterNoTargetCooldownSeconds { get; }

		internal ConfigEntry<float> TricksterInvestigateRadius { get; }

		internal ConfigEntry<float> TricksterPulseIntervalSeconds { get; }

		internal ConfigEntry<float> TricksterPlacementDistance { get; }

		internal ConfigEntry<bool> MechanicEnabled { get; }

		internal ConfigEntry<int> MechanicWeight { get; }

		internal ConfigEntry<float> MechanicRepairPercentPerSecond { get; }

		internal ConfigEntry<float> MechanicMaximumRepairPercentPerStage { get; }

		internal ConfigEntry<bool> ElectricianEnabled { get; }

		internal ConfigEntry<int> ElectricianWeight { get; }

		internal ConfigEntry<float> ElectricianChargePercentPerSecond { get; }

		internal ConfigEntry<float> ElectricianMaximumChargePercentPerStage { get; }

		internal ConfigEntry<bool> WardenEnabled { get; }

		internal ConfigEntry<int> WardenWeight { get; }

		internal ConfigEntry<float> WardenAdditionalStunSeconds { get; }

		internal ConfigEntry<bool> NinjaEnabled { get; }

		internal ConfigEntry<int> NinjaWeight { get; }

		internal ConfigEntry<float> NinjaVisionRecognitionMultiplier { get; }

		internal ConfigEntry<bool> ExecutionerEnabled { get; }

		internal ConfigEntry<int> ExecutionerWeight { get; }

		internal ConfigEntry<float> ExecutionerStunnedDamageMultiplier { get; }

		internal ConfigEntry<bool> RiderEnabled { get; }

		internal ConfigEntry<int> RiderWeight { get; }

		internal ConfigEntry<float> RiderEnemyDamageMultiplier { get; }

		internal ConfigEntry<float> RiderPlayerKnockbackMultiplier { get; }

		internal ConfigEntry<bool> InfluencerEnabled { get; }

		internal ConfigEntry<int> InfluencerWeight { get; }

		internal ConfigEntry<float> InfluencerRadius { get; }

		internal ConfigEntry<float> InfluencerPlayerCheckIntervalSeconds { get; }

		internal ConfigEntry<float> InfluencerNoiseRadiusMultiplier { get; }

		internal ConfigEntry<float> InfluencerTtsInvestigateRadius { get; }

		internal ConfigEntry<float> InfluencerMinimumTtsIntervalSeconds { get; }

		internal ConfigEntry<float> InfluencerMaximumTtsIntervalSeconds { get; }

		internal ConfigEntry<float> InfluencerTtsQuietPeriodSeconds { get; }

		internal IReadOnlyDictionary<string, ConfigEntry<string>> InfluencerUpgradeScaling { get; }

		internal ConfigEntry<bool> WerewolfEnabled { get; }

		internal ConfigEntry<int> WerewolfWeight { get; }

		internal ConfigEntry<float> WerewolfPlayerDamageMultiplier { get; }

		internal ConfigEntry<bool> BerserkerEnabled { get; }

		internal ConfigEntry<int> BerserkerWeight { get; }

		internal IReadOnlyDictionary<string, ConfigEntry<string>> BerserkerUpgradeScaling { get; }

		internal ConfigEntry<bool> BodyguardEnabled { get; }

		internal ConfigEntry<int> BodyguardWeight { get; }

		internal ConfigEntry<int> BodyguardHealthLevels { get; }

		internal ConfigEntry<float> BodyguardRadius { get; }

		internal ConfigEntry<float> BodyguardDamageSharePercent { get; }

		internal ConfigEntry<bool> RammerEnabled { get; }

		internal ConfigEntry<int> RammerWeight { get; }

		internal ConfigEntry<int> RammerTumbleDamage { get; }

		internal ConfigEntry<int> RammerSelfDamage { get; }

		internal ConfigEntry<bool> DiverEnabled { get; }

		internal ConfigEntry<int> DiverWeight { get; }

		internal ConfigEntry<float> DiverUnderfloorDurationSeconds { get; }

		internal ConfigEntry<float> DiverMovementForce { get; }

		internal ConfigEntry<bool> SniperEnabled { get; }

		internal ConfigEntry<int> SniperWeight { get; }

		internal ConfigEntry<float> SniperReferenceDistance { get; }

		internal ConfigEntry<float> SniperMinimumDamageMultiplier { get; }

		internal ConfigEntry<float> SniperMaximumDamageMultiplier { get; }

		internal ConfigEntry<float> SniperMaximumMultiplierDistance { get; }

		internal ConfigEntry<bool> ImitatorEnabled { get; }

		internal ConfigEntry<int> ImitatorWeight { get; }

		internal ConfigEntry<bool> AvengerEnabled { get; }

		internal ConfigEntry<int> AvengerWeight { get; }

		internal ConfigEntry<float> AvengerDamageMultiplier { get; }

		internal ConfigEntry<float> AvengerDurationSeconds { get; }

		internal ConfigEntry<float> AvengerTriggerRadius { get; }

		internal ConfigEntry<int> AvengerHitDamageThreshold { get; }

		internal ConfigEntry<float> AvengerHitTriggerRadius { get; }

		internal ConfigEntry<float> AvengerHitDamageMultiplier { get; }

		internal ConfigEntry<float> AvengerHitDuration { get; }

		internal ConfigEntry<float> AvengerHitCooldown { get; }

		internal ConfigEntry<bool> BrawlerEnabled { get; }

		internal ConfigEntry<int> BrawlerWeight { get; }

		internal ConfigEntry<float> BrawlerMeleeDamageMultiplier { get; }

		internal ConfigEntry<float> BrawlerComboStep { get; }

		internal ConfigEntry<float> BrawlerComboMaximum { get; }

		internal ConfigEntry<float> BrawlerComboTimeout { get; }

		internal ConfigEntry<float> BrawlerRangedDamageMultiplier { get; }

		internal ConfigEntry<bool> SuperbotEnabled { get; }

		internal ConfigEntry<bool> InfluenzaEnabled { get; }

		internal ConfigEntry<int> InfluenzaWeight { get; }

		internal ConfigEntry<float> InfluenzaIncubationSeconds { get; }

		internal ConfigEntry<int> InfluenzaMaximumHealth { get; }

		internal ConfigEntry<float> InfluenzaSneezeMinimumSeconds { get; }

		internal ConfigEntry<float> InfluenzaSneezeMaximumSeconds { get; }

		internal ConfigEntry<float> InfluenzaSneezeRange { get; }

		internal ConfigEntry<float> InfluenzaSneezeAngle { get; }

		internal ConfigEntry<float> InfluenzaSneezeChance { get; }

		internal ConfigEntry<float> InfluenzaSpeechRange { get; }

		internal ConfigEntry<float> InfluenzaSpeechAngle { get; }

		internal ConfigEntry<float> InfluenzaSpeechChance { get; }

		internal ConfigEntry<float> InfluenzaSpeechSilenceSeconds { get; }

		internal ConfigEntry<float> InfluenzaSneezeNoiseRadius { get; }

		internal ConfigEntry<bool> DisasterEnabled { get; }

		internal float ClampedBomberDistance => Mathf.Clamp(BomberDistance.Value, 1f, 100f);

		internal int ClampedBomberMaximum => Mathf.Clamp(BomberMaximumActiveGrenades.Value, 1, 30);

		internal float ClampedHunterBatteryConsumption => (float)Mathf.Clamp(HunterBatteryConsumptionPercent.Value, 0, 100) / 100f;

		internal float ClampedStinkerDistance => Mathf.Clamp(StinkerDistance.Value, 1f, 100f);

		internal float ClampedStinkerSafetyDistance => Mathf.Clamp(StinkerSafetyDistance.Value, 1f, 30f);

		internal float SniperEffectiveMaximumDistance => Math.Max(SniperReferenceDistance.Value + 0.1f, SniperMaximumMultiplierDistance.Value);

		internal float ClampedJoblessInterval => Mathf.Clamp(JoblessDamageIntervalSeconds.Value, 0.05f, 10f);

		internal float ClampedTunaDamageInterval => Mathf.Clamp(TunaDamageIntervalSeconds.Value, 0.05f, 10f);

		internal ConfigEntry<bool>? BaseUpgradeDrawEnabledEntry(string name)
		{
			if (!_baseUpgradeDrawEnabled.TryGetValue(name, out ConfigEntry<bool> value))
			{
				return null;
			}
			return value;
		}

		internal bool BaseUpgradeDrawIsEnabled(string name)
		{
			return BaseUpgradeDrawEnabledEntry(name)?.Value ?? false;
		}

		internal ConfigEntry<string>? BaseUpgradeLevelsEntry(string name)
		{
			return (ConfigEntry<string>?)(name switch
			{
				"Health" => BaseHealthLevels, 
				"Stamina" => BaseStaminaLevels, 
				"ExtraJump" => BaseExtraJumpLevels, 
				"Speed" => BaseSpeedLevels, 
				"Strength" => BaseStrengthLevels, 
				"Range" => BaseRangeLevels, 
				"Launch" => BaseLaunchLevels, 
				"TumbleClimb" => BaseTumbleClimbLevels, 
				"TumbleWings" => BaseTumbleWingsLevels, 
				"CrouchRest" => BaseCrouchRestLevels, 
				"MapPlayerCount" => BaseMapPlayerCountLevels, 
				"DeathHeadBattery" => BaseDeathHeadBatteryLevels, 
				_ => null, 
			});
		}

		internal StageRolesConfig(ConfigFile config)
		{
			//IL_0377: Unknown result type (might be due to invalid IL or missing references)
			//IL_0381: Expected O, but got Unknown
			//IL_0419: Unknown result type (might be due to invalid IL or missing references)
			//IL_0423: Expected O, but got Unknown
			//IL_0466: Unknown result type (might be due to invalid IL or missing references)
			//IL_0470: Expected O, but got Unknown
			//IL_05c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_05cf: Expected O, but got Unknown
			RoleConfigMigration.Apply(config);
			Enabled = BindBool(config, "General", "Enabled", defaultValue: true, "Enables stage role assignment.");
			UniqueRoles = BindBool(config, "General", "UniqueRoles", defaultValue: true, "Prefers different roles within the party; allows repeats when eligible roles run out. Twins still assigns one pair.");
			ShopUpgradeItemCount = BindInt(config, "General", "ShopUpgradeItemCount", 0, 0, 30, "Number of upgrade items requested for each shop.");
			EnhancedEnemyRewardsEnabled = BindBool(config, "Compatibility", "EnhancedEnemyRewardsEnabled", defaultValue: true, "Treats Elite Enemy Variants Enhanced enemies as one tier higher for Vampire healing and Hunter bonus-orb quality.");
			ShowcaseGuaranteesEnabled = BindBool(config, "Role Balance - Guarantees", "ShowcaseEnabled", defaultValue: true, "Guarantees event-focused roles according to the configured party-size tiers.");
			ShowcaseMinimumsByPlayerCount = PartySizeScaling(config, "Role Balance - Guarantees", "ShowcaseMinimums", "3:1,7:2,14:3,24:4", "Minimum Showcase roles by party size.");
			SupportGuaranteesEnabled = BindBool(config, "Role Balance - Guarantees", "SupportEnabled", defaultValue: true, "Guarantees teamwork-focused roles according to the configured party-size tiers.");
			SupportMinimumsByPlayerCount = PartySizeScaling(config, "Role Balance - Guarantees", "SupportMinimums", "4:1,8:2,14:3,24:4", "Minimum Support roles by party size.");
			RiskCombinationLimitsEnabled = BindBool(config, "Role Balance - Limits", "Enabled", defaultValue: true, "Limits simultaneous Danger and Hardship roles during normal assignment.");
			DangerRoleLimit = PartySizeScaling(config, "Role Balance - Limits", "DangerMaximums", "1:1,8:2,16:3", "Maximum simultaneous Bomber, Stinker, Werewolf and Influenza roles by party size.");
			HardshipRoleLimit = PartySizeScaling(config, "Role Balance - Limits", "HardshipMaximums", "1:1,12:2", "Maximum simultaneous Courier, Tuna and Influenza roles by party size.");
			SmallPartyCombinedRiskPlayerCount = BindInt(config, "Role Balance - Limits", "SmallPartyMaximumPlayers", 4, 1, 30, "Largest party size that uses the combined Danger and Hardship limit.");
			SmallPartyCombinedRiskRoleLimit = BindInt(config, "Role Balance - Limits", "SmallPartyCombinedMaximum", 1, 1, 30, "Maximum combined Danger and Hardship roles in a small party.");
			PreventConsecutiveSameRole = BindBool(config, "Role Balance - Variety", "PreventSameRole", defaultValue: true, "Prevents a player from receiving the same role in consecutive stages when another role is available.");
			HardshipPersonalCooldownStages = BindInt(config, "Role Balance - Variety", "HardshipCooldownStages", 2, 0, 10, "Stages after Courier, Tuna or Influenza during which that player normally cannot receive any of these Hardship roles.");
			ExcludeUnavailableContextRoles = BindBool(config, "Role Balance - Variety", "ExcludeUnavailableRoles", defaultValue: true, "Excludes context-dependent roles when their required player, enemy, weapon, or vanilla object is not available.");
			AnnouncementsEnabled = BindBool(config, "Notifications", "Enabled", defaultValue: true, "Enables role-name and role-effect chat and TTS notifications.");
			AnnouncementDelaySeconds = BindFloat(config, "Notifications", "DelaySeconds", 2.5f, 0f, 30f, "Delay before role announcements.");
			StageFluxAnnouncementDelaySeconds = BindFloat(config, "Notifications", "StageFluxDelaySeconds", 5f, 0f, 30f, "Delay used when Stage Flux is installed.");
			HudFontSize = BindInt(config, "HUD", "FontSize", 28, 16, 48, "HUD text size before overall scaling.");
			HudEnabled = BindBool(config, "HUD", "Enabled", defaultValue: true, "Shows every player's assigned role during a stage.");
			HudResourcesEnabled = BindBool(config, "HUD", "ResourceHudEnabled", defaultValue: true, "Shows your finite ability resources at the top left, independently of the role list. Requires RoleShuffle on this player and a compatible RoleShuffle host.");
			HudResourceScale = BindInt(config, "HUD", "ResourceHudScalePercent", 100, 50, 200, "Ability resource HUD scale; automatically reduced to fit the screen.");
			HudResourceOffsetX = BindInt(config, "HUD", "ResourceHudOffsetX", 0, 0, 3840, "Additional rightward offset from vanilla HP/stamina, in native HUD units. Zero aligns the icons and numbers.");
			HudResourceOffsetY = BindInt(config, "HUD", "ResourceHudOffsetY", 0, 0, 2160, "Additional downward offset below vanilla stamina, in native HUD units. Zero uses the native row spacing.");
			HudRoleDisplay = config.Bind<string>("HUD", "RoleDisplay", "NameOnly", new ConfigDescription("Role display style. Player names remain visible in every mode.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "IconAndName", "NameOnly", "IconOnly" }), Array.Empty<object>()));
			HudIconSize = BindInt(config, "HUD", "IconSize", 64, 32, 128, "Role icon size before HUD scaling. Rows and page capacity adjust to keep icons from overlapping.");
			HudAnchor = config.Bind<string>("HUD", "Anchor", "BottomLeft", new ConfigDescription("Anchor point used to position the role HUD.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[9] { "TopLeft", "TopCenter", "TopRight", "MiddleLeft", "MiddleCenter", "MiddleRight", "BottomLeft", "BottomCenter", "BottomRight" }), Array.Empty<object>()));
			HudAlignment = config.Bind<string>("HUD", "Alignment", "Left", new ConfigDescription("Horizontal alignment of the role HUD text.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "Left", "Center", "Right" }), Array.Empty<object>()));
			HudOffsetX = BindInt(config, "HUD", "OffsetX", 0, -3840, 3840, "Horizontal offset from the anchor in pixels.");
			HudOffsetY = BindInt(config, "HUD", "OffsetY", 80, -2160, 2160, "Vertical offset from the anchor in pixels.");
			HudScalePercent = BindInt(config, "HUD", "ScalePercent", 70, 50, 200, "Role HUD scale percentage.");
			HudPlayersPerPage = BindInt(config, "HUD", "PlayersPerPage", 8, 2, 20, "Maximum players shown at once, including the pinned local player. The HUD reserves room for six players when this is at least 6, including taller fallback fonts and icons.");
			HudPageIntervalSeconds = BindFloat(config, "HUD", "PageIntervalSeconds", 5f, 1f, 30f, "Seconds each role page remains visible.");
			HudTransitionDurationSeconds = BindFloat(config, "HUD", "TransitionDurationSeconds", 0.2f, 0f, 1f, "Duration of the fade between role pages.");
			HudPinLocalPlayer = BindBool(config, "HUD", "PinLocalPlayer", defaultValue: true, "Keeps the local player's role visible on every page.");
			SetRoleCommandEnabled = BindBool(config, "Testing", "Enabled", defaultValue: false, "Enables the testing commands.");
			KeepUpgradeItems = BindBool(config, "Base Upgrades", "KeepUpgradeItems", defaultValue: false, "Keeps upgrade levels gained by consuming items in this save. Default: off. Turning off stops recording and applying saved item amounts without deleting them. Role-fixed values take priority while that role is active. Throw keeps vanilla permanence and also supports AllPlayers.");
			UpgradeItemScope = config.Bind<UpgradeItemScope>("Base Upgrades", "UpgradeItemScope", REPOJP.StageRoles.UpgradeItemScope.Player, new ConfigDescription("Who receives newly consumed upgrades: Player keeps them for the user; AllPlayers grants them to everyone, including later joiners. The scope is stored when the item is used; changing it does not move earlier amounts.", (AcceptableValueBase)null, Array.Empty<object>()));
			BaseUpgradeManualAdjustmentEnabled = BindBool(config, "Base Upgrades", "ManualAdjustmentEnabled", defaultValue: false, "Allows the host to use the minus/plus buttons on BASE UPGRADES. Default: off. Adjustments are available in the lobby, truck, or shop, not during stages. Turning this off hides the buttons and excludes saved manual adjustments from base targets. Saved amounts are kept and apply again when enabled. Configured base levels and truck draws are unaffected.");
			BaseHealthLevels = BaseUpgradeScaling(config, "HealthUpgradeLevels", "1:1", 200);
			BaseStaminaLevels = BaseUpgradeScaling(config, "StaminaUpgradeLevels", string.Empty, 200);
			BaseExtraJumpLevels = BaseUpgradeScaling(config, "ExtraJumpUpgradeLevels", string.Empty, 200);
			BaseSpeedLevels = BaseUpgradeScaling(config, "SpeedUpgradeLevels", string.Empty, 200);
			BaseStrengthLevels = BaseUpgradeScaling(config, "StrengthUpgradeLevels", string.Empty, 200);
			BaseRangeLevels = BaseUpgradeScaling(config, "RangeUpgradeLevels", string.Empty, 200);
			BaseLaunchLevels = BaseUpgradeScaling(config, "LaunchUpgradeLevels", string.Empty, 200);
			BaseTumbleClimbLevels = BaseUpgradeScaling(config, "TumbleClimbUpgradeLevels", string.Empty, 200);
			BaseTumbleWingsLevels = BaseUpgradeScaling(config, "TumbleWingsUpgradeLevels", string.Empty, 200);
			BaseCrouchRestLevels = BaseUpgradeScaling(config, "CrouchRestUpgradeLevels", string.Empty, 200);
			BaseMapPlayerCountLevels = BaseUpgradeScaling(config, "MapPlayerCountUpgradeLevels", string.Empty, 1);
			BaseDeathHeadBatteryLevels = BaseUpgradeScaling(config, "DeathHeadBatteryUpgradeLevels", string.Empty, 200);
			TruckUpgradeDrawEnabled = BindBool(config, "Base Upgrade Draw", "Enabled", defaultValue: true, "After leaving the shop, runs a weighted shared Base Upgrade draw during the truck preparation phase.");
			TruckUpgradeDrawMaximumLevel = BindInt(config, "Base Upgrade Draw", "MaximumLevel", 200, 0, 200, "Maximum Base Upgrade target reachable through positive truck draws. Map Player Count remains limited to 1.");
			TruckUpgradeDrawCappedWeightMultiplier = BindFloat(config, "Base Upgrade Draw", "CappedUpgradeWeightMultiplier", 0.25f, 0f, 1f, "Minimum selection weight multiplier reached at the truck draw maximum, including configured levels and truck bonuses. Weight decreases gradually toward this value. 0 excludes capped upgrades; 1 disables the decrease. Does not allow increases beyond the maximum or change All Upgrades weight.");
			TruckUpgradeDrawWeightFalloffExponent = BindFloat(config, "Base Upgrade Draw", "WeightFalloffExponent", 2f, 0.1f, 10f, "Controls how upgrade selection weight decreases toward the draw maximum. 1 decreases evenly; larger values preserve more weight until near the maximum. All Upgrades weight is unchanged.");
			TruckUpgradeDrawDeltaWeights = config.Bind<string>("Base Upgrade Draw", "ChangeAmountWeights", "-1:10,0:15,1:60,2:15", "Comma-separated delta:weight pairs used by the truck Base Upgrade draw. Delta range: -200 to 200. Weight range: 0 to 1000. Out-of-range numeric values are clamped, malformed pairs are ignored, and duplicate deltas use the last value.");
			BindTruckUpgradeDrawWeight(config, "Health");
			BindTruckUpgradeDrawWeight(config, "Stamina");
			BindTruckUpgradeDrawWeight(config, "ExtraJump");
			BindTruckUpgradeDrawWeight(config, "Speed");
			BindTruckUpgradeDrawWeight(config, "Strength");
			BindTruckUpgradeDrawWeight(config, "Range");
			BindTruckUpgradeDrawWeight(config, "Launch");
			BindTruckUpgradeDrawWeight(config, "TumbleClimb");
			BindTruckUpgradeDrawWeight(config, "TumbleWings");
			BindTruckUpgradeDrawWeight(config, "CrouchRest");
			BindTruckUpgradeDrawWeight(config, "MapPlayerCount");
			BindTruckUpgradeDrawWeight(config, "DeathHeadBattery");
			BindTruckUpgradeDrawWeight(config, "AllUpgrades");
			TankEnabled = RoleEnabled(config, "Tank");
			TankWeight = RoleWeight(config, "Tank");
			TankHealthLevels = BindInt(config, "Tank", "HealthUpgradeLevels", 21, 0, 200, "Minimum Health upgrade level guaranteed to Tank; higher Base and multiplier targets take priority.");
			TankHealthMultiplier = BindFloat(config, "Tank", "HealthMultiplier", 1.5f, 1f, 10f, "Multiply Base maximum HP (100 + 20 per level), then round up to an upgrade level.");
			TankMaximumHealth = BindInt(config, "Tank", "MaximumHealth", 4100, 100, 4100, "Maximum HP for multiplier growth: ceil(maximum over vanilla levels 0-200). Existing Base and configured role minimum remain guaranteed.");
			RunnerEnabled = RoleEnabled(config, "Runner");
			RunnerWeight = RoleWeight(config, "Runner");
			RunnerSpeedLevels = BindInt(config, "Runner", "SpeedUpgradeLevels", 6, 0, 200, "Minimum Speed upgrade level guaranteed to Runner; higher Base and multiplier targets take priority.");
			RunnerStaminaLevels = BindInt(config, "Runner", "StaminaUpgradeLevels", 46, 0, 200, "Minimum Stamina upgrade level guaranteed to Runner; higher Base and multiplier targets take priority.");
			RunnerSpeedMultiplier = BindFloat(config, "Runner", "SpeedMultiplier", 1.5f, 1f, 10f, "Multiply Base sprint speed (5 + level), then round up to an upgrade level.");
			RunnerStaminaMultiplier = BindFloat(config, "Runner", "StaminaMultiplier", 1.5f, 1f, 10f, "Multiply Base stamina capacity (40 + 10 per level), then round up to an upgrade level. Sprint duration also depends on speed.");
			RunnerMaximumSpeed = BindInt(config, "Runner", "MaximumSprintSpeed", 205, 5, 205, "Maximum sprint speed for multiplier growth: ceil(maximum over vanilla levels 0-200). Existing Base and configured role minimum remain guaranteed.");
			RunnerMaximumStamina = BindInt(config, "Runner", "MaximumStamina", 2040, 40, 2040, "Maximum stamina capacity for multiplier growth: ceil(maximum over vanilla levels 0-200). Existing Base and configured role minimum remain guaranteed.");
			JumperEnabled = RoleEnabled(config, "Jumper");
			JumperWeight = RoleWeight(config, "Jumper");
			JumperExtraJumpLevels = UpgradeLevel(config, "Jumper", "ExtraJumpUpgradeLevels", 10);
			LifterEnabled = RoleEnabled(config, "Lifter");
			LifterWeight = RoleWeight(config, "Lifter");
			LifterStrengthLevel = BindInt(config, "Lifter", "StrengthLevel", 200, 0, 200, "Strength upgrade level granted while Lifter is active. Heavy objects, carts and bikes use normal game physics at this level. The normal upgrade level is restored when the role ends.");
			LifterLightItemStrengthLevel = BindInt(config, "Lifter", "LightItemStrengthLevel", 1, 0, 200, "Strength level used to hold light objects and shop equipment, including weapons but excluding carts and bikes. Default 1 reduces shaking. Does not change the displayed Strength level or weapon attack bonuses.");
			LauncherEnabled = RoleEnabled(config, "Launcher");
			LauncherWeight = RoleWeight(config, "Launcher");
			LauncherLaunchLevels = UpgradeLevel(config, "Launcher", "LaunchUpgradeLevels", 10);
			ClimberEnabled = RoleEnabled(config, "Climber");
			ClimberWeight = RoleWeight(config, "Climber");
			ClimberClimbLevels = UpgradeLevel(config, "Climber", "ClimbUpgradeLevels", 50);
			ClimberRangeLevels = UpgradeLevel(config, "Climber", "RangeUpgradeLevels", 20);
			FlyerEnabled = RoleEnabled(config, "Flyer");
			FlyerWeight = RoleWeight(config, "Flyer");
			FlyerWingsLevels = UpgradeLevel(config, "Flyer", "WingsUpgradeLevels", 10);
			TrackerEnabled = RoleEnabled(config, "Tracker");
			TrackerWeight = RoleWeight(config, "Tracker");
			TrackerHealthLevels = BindInt(config, "Tracker", "HealthUpgradeLevels", 3, 0, 200, "Minimum Health upgrade level while Tracker is assigned. Stronger Base upgrades are preserved.");
			TrackerEnemyRange = BindFloat(config, "Tracker", "EnemyDetectionRange", 25f, 1f, 100f, "Automatically reports the nearest enemy within this distance, even through walls.");
			TrackerDangerRange = BindFloat(config, "Tracker", "DangerAlertRange", 8f, 0f, 100f, "Warns when an enemy approaches within this distance, capped at EnemyDetectionRange. Zero disables close warnings.");
			TrackerNotificationInterval = BindFloat(config, "Tracker", "NotificationIntervalSeconds", 20f, 5f, 120f, "Minimum seconds between ordinary reports. Close warnings can bypass this wait; repeated close warnings have the same minimum interval.");
			TrackerDistanceChange = BindFloat(config, "Tracker", "DistanceChangeThreshold", 5f, 1f, 50f, "Distance change needed for an updated report. Target changes, direction changes of at least 60 degrees, or changes above/below also qualify.");
			GhostEnabled = RoleEnabled(config, "Ghost");
			GhostWeight = RoleWeight(config, "Ghost", 80);
			GhostDeathHeadBatteryLevels = UpgradeLevel(config, "Ghost", "DeathHeadBatteryUpgradeLevels", 50);
			GhostHealRadius = BindFloat(config, "Ghost", "HealRadius", 5f, 0f, 30f, "Radius around the dead Ghost's head in which living allies are healed.");
			GhostHealAmount = BindInt(config, "Ghost", "HealAmount", 1, 0, 100, "HP restored to each nearby ally per healing interval.");
			GhostHealInterval = BindFloat(config, "Ghost", "HealIntervalSeconds", 2f, 0.5f, 60f, "Seconds between healing pulses while Ghost is dead.");
			GhostTotalHealingLimit = BindInt(config, "Ghost", "TotalHealingLimit", 50, 0, 10000, "Total HP Ghost can restore per stage, shared among allies and retained through revival.");
			GhostCarrierSpeedBonus = BindInt(config, "Ghost", "CarrierSpeedBonusLevels", 1, 0, 20, "Temporary Speed levels for players grabbing Ghost's head. Multiple heads do not stack; uses the stronger of this and King's Speed aura.");
			BomberEnabled = RoleEnabled(config, "Bomber");
			BomberWeight = RoleWeight(config, "Bomber", 80);
			BomberDistance = BindFloat(config, "Bomber", "DistancePerGrenade", 8f, 1f, 100f, "Travel distance required per grenade.");
			BomberMaximumActiveGrenades = BindInt(config, "Bomber", "MaximumActiveGrenades", 30, 1, 30, "Maximum retained generated grenades per Bomber. Placing another removes the oldest grenade.");
			BomberAllowTruckSpawns = BindBool(config, "Bomber", "AllowTruckSpawns", defaultValue: false, "Allows grenade placement inside the truck.");
			BomberExplosiveEnabled = BindBool(config, "Bomber", "ExplosiveGrenadesEnabled", defaultValue: true, "Includes the vanilla explosive grenade.");
			BomberStunEnabled = BindBool(config, "Bomber", "StunGrenadesEnabled", defaultValue: true, "Includes the vanilla stun grenade.");
			BomberShockwaveEnabled = BindBool(config, "Bomber", "ShockwaveGrenadesEnabled", defaultValue: true, "Includes the vanilla shockwave grenade.");
			BomberDuctTapedEnabled = BindBool(config, "Bomber", "DuctTapedGrenadesEnabled", defaultValue: true, "Includes the vanilla duct-taped grenade.");
			MedicEnabled = RoleEnabled(config, "Medic");
			MedicWeight = RoleWeight(config, "Medic");
			MedicHealAmount = BindInt(config, "Medic", "HealAmount", 5, 1, 100, "HP restored to each nearby living ally per tick; excludes the Medic.");
			MedicHealIntervalSeconds = BindFloat(config, "Medic", "HealIntervalSeconds", 2f, 0.1f, 30f, "Seconds between Medic healing ticks.");
			MedicHealRadius = BindFloat(config, "Medic", "HealRadius", 5f, 1f, 30f, "Maximum distance from the Medic for healing.");
			MedicTotalHealingLimit = BindInt(config, "Medic", "TotalHealingLimit", 150, 1, 10000, "Maximum total health restored by each Medic per stage.");
			PhoenixEnabled = RoleEnabled(config, "Phoenix");
			PhoenixWeight = RoleWeight(config, "Phoenix");
			PhoenixRevivalHealth = BindInt(config, "Phoenix", "RevivalHealth", 25, 1, 1000, "Health restored after Phoenix revival, capped at the player's maximum health.");
			PhoenixReviveDelaySeconds = BindFloat(config, "Phoenix", "ReviveDelaySeconds", 2f, 2f, 10f, "Minimum delay after death before Phoenix can revive. The death head must also be stationary; moving heads can make the wait longer.");
			JoblessEnabled = RoleEnabled(config, "Courier");
			JoblessWeight = RoleWeight(config, "Courier", 20);
			JoblessDamage = BindInt(config, "Courier", "Damage", 1, 1, 100, "Damage applied per tick outside the truck.");
			JoblessDamageIntervalSeconds = BindFloat(config, "Courier", "DamageIntervalSeconds", 0.1f, 0.05f, 10f, "Seconds between damage ticks outside the truck.");
			JoblessContractDistance = BindFloat(config, "Courier", "ContractDistance", 5f, 1f, 50f, "Carry a different positive-value valuable this far outside delivery areas, then bring it into the truck or an extraction point. No stage limit; each valuable rewards each worker once.");
			JoblessInitialGrace = BindFloat(config, "Courier", "InitialGraceSeconds", 120f, 0f, 300f, "Time without Courier attrition at stage start and after each revival. Longer remaining delivery protection is preserved.");
			JoblessTinyGrace = BindFloat(config, "Courier", "TinyGraceSeconds", 90f, 0f, 300f, "Attrition exemption after delivering a vanilla Tiny valuable. Healing still applies at zero seconds.");
			JoblessSmallGrace = BindFloat(config, "Courier", "SmallGraceSeconds", 120f, 0f, 300f, "Attrition exemption after delivering a vanilla Small valuable.");
			JoblessMediumGrace = BindFloat(config, "Courier", "MediumGraceSeconds", 180f, 0f, 300f, "Attrition exemption after delivering a vanilla Medium valuable.");
			JoblessBigGrace = BindFloat(config, "Courier", "BigGraceSeconds", 240f, 0f, 300f, "Attrition exemption after delivering a vanilla Big valuable.");
			JoblessWideGrace = BindFloat(config, "Courier", "WideGraceSeconds", 240f, 0f, 300f, "Attrition exemption after delivering a vanilla Wide valuable.");
			JoblessTallGrace = BindFloat(config, "Courier", "TallGraceSeconds", 240f, 0f, 300f, "Attrition exemption after delivering a vanilla Tall valuable.");
			JoblessVeryTallGrace = BindFloat(config, "Courier", "VeryTallGraceSeconds", 300f, 0f, 300f, "Attrition exemption after delivering a vanilla VeryTall valuable.");
			JoblessTinyHeal = BindInt(config, "Courier", "TinyHealAmount", 10, 0, 10000, "Fixed HP restored after delivering a Tiny valuable, capped by maximum HP.");
			JoblessSmallHeal = BindInt(config, "Courier", "SmallHealAmount", 25, 0, 10000, "Fixed HP restored after delivering a Small valuable, capped by maximum HP.");
			JoblessMediumHeal = BindInt(config, "Courier", "MediumHealAmount", 50, 0, 10000, "Fixed HP restored after delivering a Medium valuable, capped by maximum HP.");
			JoblessBigHeal = BindInt(config, "Courier", "BigHealAmount", 100, 0, 10000, "Fixed HP restored after delivering a Big valuable, capped by maximum HP.");
			JoblessWideHeal = BindInt(config, "Courier", "WideHealAmount", 100, 0, 10000, "Fixed HP restored after delivering a Wide valuable, capped by maximum HP.");
			JoblessTallHeal = BindInt(config, "Courier", "TallHealAmount", 100, 0, 10000, "Fixed HP restored after delivering a Tall valuable, capped by maximum HP.");
			JoblessVeryTallHeal = BindInt(config, "Courier", "VeryTallHealAmount", 100, 0, 10000, "Fixed HP restored after delivering a VeryTall valuable, capped by maximum HP.");
			RescuerEnabled = RoleEnabled(config, "Rescuer");
			RescuerWeight = RoleWeight(config, "Rescuer", 80);
			RescuerRevivalHealth = BindInt(config, "Rescuer", "RevivalHealth", 25, 1, 1000, "Health restored to a player revived by Rescuer, capped at the player's maximum health.");
			RescuerReviveDelaySeconds = BindFloat(config, "Rescuer", "ReviveDelaySeconds", 2f, 0f, 10f, "Minimum time since the target died before grabbing their death head can revive them. This is not a continuous grab duration. Moving heads are allowed.");
			RescuerMaximumRevives = BindInt(config, "Rescuer", "MaximumRevives", 2, 1, 10, "Maximum revivals per stage.");
			VampireEnabled = RoleEnabled(config, "Vampire");
			VampireWeight = RoleWeight(config, "Vampire");
			VampireTier1HealAmount = BindInt(config, "Vampire", "Tier1HealAmount", 5, 1, 100, "Health restored by a nearby Tier 1 enemy death.");
			VampireTier2HealAmount = BindInt(config, "Vampire", "Tier2HealAmount", 10, 1, 100, "Health restored by a nearby Tier 2 enemy death.");
			VampireTier3HealAmount = BindInt(config, "Vampire", "Tier3HealAmount", 50, 1, 100, "Health restored by a nearby Tier 3 enemy death.");
			VampireRadius = BindFloat(config, "Vampire", "Radius", 10f, 1f, 50f, "Maximum distance from a dying enemy.");
			KingEnabled = RoleEnabled(config, "King");
			KingWeight = RoleWeight(config, "King");
			KingUpgradeRadius = BindFloat(config, "King", "UpgradeRadius", 12f, 1f, 30f, "Radius in metres for temporary upgrade support to living allies; excludes King.");
			KingSpeedBonus = BindInt(config, "King", "SpeedBonusLevels", 2, 0, 200, "Temporary Speed levels for allies in range, capped at level 200. Removed on leaving the aura.");
			KingRangeBonus = BindInt(config, "King", "RangeBonusLevels", 2, 0, 200, "Temporary Range levels for allies in range, capped at level 200. Removed on leaving the aura.");
			KingStrengthBonus = BindInt(config, "King", "StrengthBonusLevels", 5, 0, 200, "Temporary Strength upgrade levels for allies in range, capped at level 200. Removed when they leave the aura.");
			TunaEnabled = RoleEnabled(config, "Tuna");
			TunaWeight = RoleWeight(config, "Tuna", 50);
			TunaStationaryDelaySeconds = BindFloat(config, "Tuna", "StationaryDelaySeconds", 3f, 0.1f, 30f, "Seconds without movement before damage starts.");
			TunaDamage = BindInt(config, "Tuna", "Damage", 1, 1, 100, "Damage applied per stationary tick.");
			TunaDamageIntervalSeconds = BindFloat(config, "Tuna", "DamageIntervalSeconds", 0.1f, 0.05f, 10f, "Seconds between stationary damage ticks.");
			MusicianEnabled = RoleEnabled(config, "Musician");
			MusicianWeight = RoleWeight(config, "Musician", 60);
			MusicianHealAmount = BindInt(config, "Musician", "HealAmount", 5, 1, 100, "HP per healing pulse for the Musician and each nearby living player while playing.");
			MusicianHealRadius = BindFloat(config, "Musician", "HealRadius", 10f, 1f, 50f, "Maximum healing distance from the Musician while playing.");
			MusicianHealCooldownSeconds = BindFloat(config, "Musician", "HealCooldownSeconds", 1f, 0.1f, 30f, "Minimum seconds between healing pulses, shared by all instruments played by this Musician.");
			MusicianTotalHealingLimit = BindInt(config, "Musician", "TotalHealingLimit", 1000, 1, 10000, "Combined healing budget for self and allies per stage. Death, revival and role changes do not reset it.");
			MageEnabled = RoleEnabled(config, "Mage");
			MageWeight = RoleWeight(config, "Mage");
			MageCastIntervalSeconds = BindFloat(config, "Mage", "CastIntervalSeconds", 3f, 0.1f, 30f, "Minimum seconds between spell activations.");
			MageBeamDurationSeconds = BindFloat(config, "Mage", "BeamDurationSeconds", 5f, 0.1f, 30f, "Seconds the Mage laser spell follows the caster's view while firing. Does not change attacks from held staffs.");
			MageStarExpression = ExpressionName(config, "Mage", "StarExpression", "Angry", "Star Wand");
			MageRollExpression = ExpressionName(config, "Mage", "RollExpression", "Sad", "Roll Staff");
			MageGravityExpression = ExpressionName(config, "Mage", "GravityExpression", "Suspicious", "Zero Gravity Staff");
			MageVoidExpression = ExpressionName(config, "Mage", "VoidExpression", "EyesClosed", "Void Staff");
			MageLaserExpression = ExpressionName(config, "Mage", "LaserExpression", "Scared", "Wizard Staff beam");
			MageAutoRecoveryEnabled = BindBool(config, "Mage", "AutoRecoveryEnabled", defaultValue: true, "Enables Mage health recovery after avoiding damage.");
			MageAutoRecoveryDelaySeconds = BindFloat(config, "Mage", "AutoRecoveryDelaySeconds", 10f, 0f, 300f, "Seconds without taking damage before automatic health recovery starts.");
			MageAutoRecoveryIntervalSeconds = BindFloat(config, "Mage", "AutoRecoveryIntervalSeconds", 2f, 0.1f, 60f, "Seconds between automatic Mage health recovery ticks.");
			MageAutoRecoveryAmount = BindInt(config, "Mage", "AutoRecoveryAmount", 1, 0, 100, "Health restored by each automatic Mage recovery tick.");
			MageAutoRecoveryTotalHealingLimit = BindInt(config, "Mage", "AutoRecoveryTotalHealingLimit", 120, 1, 10000, "Maximum total health restored by each player's Mage automatic recovery per stage. Other healing is not counted.");
			MageStarHealthCost = BindInt(config, "Mage", "StarHealthCost", 10, 0, 100, "Health consumed after a successful Star Wand cast.");
			MageGravityHealthCost = BindInt(config, "Mage", "GravityHealthCost", 10, 0, 100, "Health consumed after a successful Zero Gravity Staff cast.");
			MageRollHealthCost = BindInt(config, "Mage", "RollHealthCost", 15, 0, 100, "Health consumed after a successful Roll Staff cast.");
			MageVoidHealthCost = BindInt(config, "Mage", "VoidHealthCost", 30, 0, 100, "Health consumed after a successful Void Staff cast.");
			MageLaserHealthCost = BindInt(config, "Mage", "LaserHealthCost", 50, 0, 100, "Health consumed after a successful Wizard Staff beam cast.");
			GamblerEnabled = RoleEnabled(config, "Gambler");
			GamblerWeight = RoleWeight(config, "Gambler");
			GamblerWinChancePercent = BindInt(config, "Gambler", "WinChancePercent", 50, 0, 100, "Chance for a wagered valuable to use the win multiplier.");
			GamblerWinValueMultiplier = BindFloat(config, "Gambler", "WinValueMultiplier", 2f, 0f, 10f, "Value multiplier applied when a wager wins.");
			GamblerGambitGreenHealAmount = BindInt(config, "Gambler", "GambitGreenHealAmount", 50, 25, 1000, "Total health restored when Gambit lands on green for a Gambler.");
			GamblerGambitRedDamage = BindInt(config, "Gambler", "GambitRedDamage", 100, 50, 1000, "Total damage dealt when Gambit lands on red for a Gambler.");
			GamblerGambitWhiteHealthUpgradeLevels = BindInt(config, "Gambler", "GambitWhiteHealthUpgradeLevels", 5, 0, 200, "Temporary Health upgrade levels added when Gambit lands on white for a Gambler.");
			HunterEnabled = RoleEnabled(config, "Hunter");
			HunterWeight = RoleWeight(config, "Hunter", 80);
			HunterBatteryConsumptionPercent = BindInt(config, "Hunter", "WeaponBatteryConsumptionPercent", 75, 0, 100, "Percentage of normal weapon battery consumption used by Hunter.");
			HunterDoubleOrbChancePercent = BindFloat(config, "Hunter", "DoubleOrbChancePercent", 10f, 0f, 100f, "Chance for an enemy defeated by Hunter to drop twice its normal orb count.");
			HunterJackpotOrbChancePercent = BindFloat(config, "Hunter", "JackpotOrbChancePercent", 0.5f, 0f, 100f, "Chance for an enemy defeated by Hunter to drop the configured jackpot orb count.");
			HunterJackpotOrbCount = BindInt(config, "Hunter", "JackpotOrbCount", 10, 1, 30, "Orb count used when Hunter's jackpot roll succeeds.");
			HunterGuaranteedOrbEveryKills = BindInt(config, "Hunter", "GuaranteedOrbEveryKills", 5, 0, 100, "Every this many personal kills that drop normal orbs guarantees at least one extra orb. Random rewards count toward the guarantee. Zero disables it. The count resets each stage.");
			StinkerEnabled = RoleEnabled(config, "Stinker");
			StinkerWeight = RoleWeight(config, "Stinker", 80);
			StinkerDistance = BindFloat(config, "Stinker", "DistancePerCloud", 2f, 1f, 100f, "Travel distance required per uranium cloud.");
			StinkerSafetyDistance = BindFloat(config, "Stinker", "MinimumSafetyDistance", 2f, 1f, 30f, "Minimum distance from the newest pending cloud when it is created.");
			StinkerAllowTruckSpawns = BindBool(config, "Stinker", "AllowTruckSpawns", defaultValue: false, "Allows uranium cloud creation inside the truck.");
			StinkerBreakGraceSeconds = BindFloat(config, "Stinker", "BreakGraceSeconds", 0.5f, 0f, 10f, "Seconds from a spawned uranium valuable being ready until it breaks. Applies to newly spawned valuables; clouds remain automatic.");
			EngineerEnabled = RoleEnabled(config, "Engineer");
			EngineerWeight = RoleWeight(config, "Engineer", 70);
			TricksterEnabled = RoleEnabled(config, "Trickster");
			TricksterWeight = RoleWeight(config, "Trickster");
			TricksterExpression = ExpressionName(config, "Trickster", "DecoyExpression", "Happy", "decoy placement");
			TricksterActiveSeconds = BindFloat(config, "Trickster", "ActiveSeconds", 25f, 1f, 120f, "Seconds before the active decoy is removed.");
			TricksterCooldownSeconds = BindFloat(config, "Trickster", "CooldownSeconds", 45f, 1f, 300f, "Seconds after an effective decoy ends before another can be placed.");
			TricksterNoTargetCooldownSeconds = BindFloat(config, "Trickster", "NoTargetCooldownSeconds", 10f, 0f, 300f, "Seconds after a decoy that attracted no enemies ends before another can be placed.");
			TricksterInvestigateRadius = BindFloat(config, "Trickster", "InvestigateRadius", 40f, 1f, 100f, "Enemy investigation radius around the decoy.");
			TricksterPulseIntervalSeconds = BindFloat(config, "Trickster", "PulseIntervalSeconds", 1f, 0.25f, 10f, "Seconds between enemy investigation pulses.");
			TricksterPlacementDistance = BindFloat(config, "Trickster", "PlacementDistance", 2f, 0.5f, 10f, "Distance in front of Trickster used for decoy placement.");
			MechanicEnabled = RoleEnabled(config, "Mechanic");
			MechanicWeight = RoleWeight(config, "Mechanic");
			MechanicRepairPercentPerSecond = BindFloat(config, "Mechanic", "RepairPercentPerSecond", 2f, 0f, 100f, "Percentage of a held valuable's original value repaired per second.");
			MechanicMaximumRepairPercentPerStage = BindFloat(config, "Mechanic", "MaximumRepairPercentPerStage", 50f, 0f, 100f, "Maximum cumulative original-value percentage repaired by each Mechanic per stage.");
			ElectricianEnabled = RoleEnabled(config, "Electrician");
			ElectricianWeight = RoleWeight(config, "Electrician", 80);
			ElectricianChargePercentPerSecond = BindFloat(config, "Electrician", "ChargePercentPerSecond", 10f, 0f, 100f, "Battery percentage restored per second to held inactive rechargeable items.");
			ElectricianMaximumChargePercentPerStage = BindFloat(config, "Electrician", "MaximumChargePercentPerStage", 100f, 0f, 1000f, "Maximum cumulative battery percentage restored by each Electrician per stage.");
			WardenEnabled = RoleEnabled(config, "Warden");
			WardenWeight = RoleWeight(config, "Warden");
			WardenAdditionalStunSeconds = BindFloat(config, "Warden", "AdditionalStunSeconds", 3f, 0f, 30f, "Seconds added to enemy stuns caused by Warden.");
			NinjaEnabled = RoleEnabled(config, "Ninja");
			NinjaWeight = RoleWeight(config, "Ninja");
			NinjaVisionRecognitionMultiplier = BindFloat(config, "Ninja", "VisionRecognitionMultiplier", 2f, 1f, 10f, "Multiplier applied to the time enemies need to visually recognize Ninja. Player-generated investigation sounds remain suppressed.");
			ExecutionerEnabled = RoleEnabled(config, "Executioner");
			ExecutionerWeight = RoleWeight(config, "Executioner");
			ExecutionerStunnedDamageMultiplier = BindFloat(config, "Executioner", "StunnedDamageMultiplier", 2f, 1f, 10f, "Damage multiplier for direct attacks by Executioner against enemies that were already stunned.");
			RiderEnabled = RoleEnabled(config, "Rider");
			RiderWeight = RoleWeight(config, "Rider", 60);
			RiderEnemyDamageMultiplier = BindFloat(config, "Rider", "EnemyDamageMultiplier", 3f, 1f, 10f, "Multiplier for vanilla vehicle impact damage against enemies while Rider is driving.");
			RiderPlayerKnockbackMultiplier = BindFloat(config, "Rider", "PlayerKnockbackMultiplier", 2f, 1f, 10f, "Multiplier for existing vanilla vehicle Tumble knockback against players while Rider is driving.");
			InfluencerEnabled = RoleEnabled(config, "Influencer");
			InfluencerWeight = RoleWeight(config, "Influencer");
			InfluencerRadius = BindFloat(config, "Influencer", "PlayerRadius", 20f, 1f, 100f, "Radius used to count nearby living players.");
			InfluencerPlayerCheckIntervalSeconds = BindFloat(config, "Influencer", "PlayerCheckIntervalSeconds", 0.5f, 0.1f, 10f, "Seconds between nearby-player and upgrade checks.");
			InfluencerNoiseRadiusMultiplier = BindFloat(config, "Influencer", "NoiseRadiusMultiplier", 2f, 1f, 10f, "Multiplier applied to vanilla footstep, landing, voice, and chat TTS investigation radii.");
			InfluencerTtsInvestigateRadius = BindFloat(config, "Influencer", "TTSInvestigateRadius", 20f, 1f, 100f, "Enemy investigation radius produced by periodic Influencer TTS.");
			InfluencerMinimumTtsIntervalSeconds = BindFloat(config, "Influencer", "MinimumTTSIntervalSeconds", 20f, 1f, 300f, "Minimum randomly selected interval between periodic TTS messages.");
			InfluencerMaximumTtsIntervalSeconds = BindFloat(config, "Influencer", "MaximumTTSIntervalSeconds", 40f, 1f, 300f, "Maximum randomly selected interval between periodic TTS messages.");
			InfluencerTtsQuietPeriodSeconds = BindFloat(config, "Influencer", "TTSQuietPeriodSeconds", 1.5f, 0f, 10f, "Required quiet period after this player's TTS before Influencer may speak.");
			InfluencerUpgradeScaling = UpgradeScalingEntries(config, "Influencer", useHealthPercent: false, includeHealth: true);
			WerewolfEnabled = RoleEnabled(config, "Werewolf");
			WerewolfWeight = RoleWeight(config, "Werewolf", 40);
			WerewolfPlayerDamageMultiplier = BindFloat(config, "Werewolf", "PlayerDamageMultiplier", 2f, 1f, 10f, "Multiplier for identifiable player-origin damage dealt by Werewolf to another player.");
			BerserkerEnabled = RoleEnabled(config, "Berserker");
			BerserkerWeight = RoleWeight(config, "Berserker");
			BerserkerUpgradeScaling = UpgradeScalingEntries(config, "Berserker", useHealthPercent: true, includeHealth: false);
			BodyguardEnabled = RoleEnabled(config, "Bodyguard");
			BodyguardWeight = RoleWeight(config, "Bodyguard", 80);
			BodyguardHealthLevels = UpgradeLevel(config, "Bodyguard", "HealthUpgradeLevels", 11);
			BodyguardRadius = BindFloat(config, "Bodyguard", "ProtectionRadius", 15f, 1f, 100f, "Maximum distance from a player for Bodyguard to absorb enemy damage.");
			BodyguardDamageSharePercent = BindFloat(config, "Bodyguard", "DamageSharePercent", 50f, 0f, 100f, "Percentage of enemy damage transferred to Bodyguard, without reducing Bodyguard below 1 HP.");
			RammerEnabled = RoleEnabled(config, "Rammer");
			RammerWeight = RoleWeight(config, "Rammer");
			RammerTumbleDamage = BindInt(config, "Rammer", "TumbleAttackDamage", 100, 0, 100000, "Enemy damage dealt by Rammer's Tumble Attack.");
			RammerSelfDamage = BindInt(config, "Rammer", "SelfDamage", 15, 0, 100000, "Damage Rammer takes after its Tumble Attack hits an enemy.");
			DiverEnabled = RoleEnabled(config, "Diver");
			DiverWeight = RoleWeight(config, "Diver");
			DiverUnderfloorDurationSeconds = BindFloat(config, "Diver", "UnderfloorDurationSeconds", 10f, 1f, 120f, "Maximum seconds Diver can remain below a floor before dying.");
			DiverMovementForce = BindFloat(config, "Diver", "MovementForce", 8f, 1f, 30f, "Force used for free movement while Diver is below a floor.");
			SniperEnabled = RoleEnabled(config, "Sniper");
			SniperWeight = RoleWeight(config, "Sniper");
			SniperReferenceDistance = BindFloat(config, "Sniper", "ReferenceDistance", 6f, 1f, 50f, "Distance in meters where Sniper deals normal damage.");
			SniperMinimumDamageMultiplier = BindFloat(config, "Sniper", "MinimumDamageMultiplier", 0.5f, 0f, 1f, "Damage multiplier at zero distance.");
			SniperMaximumDamageMultiplier = BindFloat(config, "Sniper", "MaximumDamageMultiplier", 2f, 1f, 10f, "Maximum long-range damage multiplier.");
			SniperMaximumMultiplierDistance = BindFloat(config, "Sniper", "MaximumMultiplierDistance", 18f, 1f, 100f, "Distance in metres where Sniper reaches maximum damage. If this is not above ReferenceDistance, the effective distance is ReferenceDistance + 0.1 m.");
			ImitatorEnabled = RoleEnabled(config, "Imitator");
			ImitatorWeight = RoleWeight(config, "Imitator");
			AvengerEnabled = RoleEnabled(config, "Avenger");
			AvengerWeight = RoleWeight(config, "Avenger");
			AvengerDamageMultiplier = BindFloat(config, "Avenger", "DamageMultiplier", 1.5f, 1f, 10f, "Enemy damage multiplier after another player dies.");
			AvengerDurationSeconds = BindFloat(config, "Avenger", "DurationSeconds", 30f, 1f, 120f, "Duration of Avenger's enemy damage bonus after another player dies.");
			AvengerTriggerRadius = BindFloat(config, "Avenger", "TriggerRadius", 30f, 1f, 100f, "Maximum distance in meters from a dying player that activates Avenger.");
			AvengerHitDamageThreshold = BindInt(config, "Avenger", "AllyHitDamageThreshold", 15, 1, 1000, "Minimum HP lost by an ally in one confirmed enemy hit to trigger the lesser damage bonus.");
			AvengerHitTriggerRadius = BindFloat(config, "Avenger", "AllyHitTriggerRadius", 15f, 1f, 100f, "Maximum distance from the ally taking enemy damage.");
			AvengerHitDamageMultiplier = BindFloat(config, "Avenger", "AllyHitDamageMultiplier", 1.25f, 1f, 10f, "Enemy damage multiplier triggered by an ally being hit; uses the stronger active bonus without stacking.");
			AvengerHitDuration = BindFloat(config, "Avenger", "AllyHitDurationSeconds", 10f, 1f, 120f, "Duration of the damage bonus triggered by an ally being hit.");
			AvengerHitCooldown = BindFloat(config, "Avenger", "AllyHitCooldownSeconds", 20f, 1f, 300f, "Cooldown between ally-hit activations; nearby deaths bypass this cooldown.");
			BrawlerEnabled = RoleEnabled(config, "Brawler");
			BrawlerWeight = RoleWeight(config, "Brawler");
			BrawlerMeleeDamageMultiplier = BindFloat(config, "Brawler", "MeleeDamageMultiplier", 1.25f, 0f, 10f, "Damage multiplier for identifiable melee weapon attacks.");
			BrawlerRangedDamageMultiplier = BindFloat(config, "Brawler", "RangedDamageMultiplier", 0.75f, 0f, 10f, "Damage multiplier for identifiable ranged weapon attacks.");
			BrawlerComboStep = BindFloat(config, "Brawler", "ComboMultiplierStep", 0.25f, 0f, 5f, "Extra melee multiplier per consecutive confirmed hit against the same enemy. Does not affect friendly fire.");
			BrawlerComboMaximum = BindFloat(config, "Brawler", "ComboMaximumMultiplier", 2f, 0f, 10f, "Maximum melee combo multiplier, never below MeleeDamageMultiplier.");
			BrawlerComboTimeout = BindFloat(config, "Brawler", "ComboTimeoutSeconds", 4f, 0.1f, 30f, "Seconds without a successful hit before the melee combo resets. Hitting a different enemy also resets it.");
			InfluenzaEnabled = RoleEnabled(config, "Influenza");
			InfluenzaWeight = RoleWeight(config, "Influenza", 20);
			InfluenzaIncubationSeconds = BindFloat(config, "Influenza", "IncubationSeconds", 30f, 0f, 300f, "Seconds until symptoms begin after assignment or infection. Applies to new infections; existing timers are kept.");
			InfluenzaMaximumHealth = BindInt(config, "Influenza", "MaximumHealth", 75, 1, 10000, "Maximum HP forced after symptoms begin. Raising this value does not heal the player.");
			InfluenzaSneezeMinimumSeconds = BindFloat(config, "Influenza", "SneezeMinimumSeconds", 30f, 1f, 600f, "Shortest random sneeze interval after symptoms begin. The two interval values are sorted; changes apply when the next sneeze is scheduled.");
			InfluenzaSneezeMaximumSeconds = BindFloat(config, "Influenza", "SneezeMaximumSeconds", 90f, 1f, 600f, "Longest random sneeze interval after symptoms begin. Equal interval values give a regular interval.");
			InfluenzaSneezeRange = BindFloat(config, "Influenza", "SneezeRange", 5f, 0.1f, 30f, "Maximum distance in metres for sneeze infection.");
			InfluenzaSneezeAngle = BindFloat(config, "Influenza", "SneezeAngleDegrees", 40f, 1f, 360f, "Full horizontal infection angle in front when sneezing. 40 means 20 degrees to each side.");
			InfluenzaSneezeChance = BindFloat(config, "Influenza", "SneezeChancePercent", 60f, 0f, 100f, "Infection chance for each eligible teammate per sneeze. Zero prevents sneeze infection.");
			InfluenzaSpeechRange = BindFloat(config, "Influenza", "SpeechRange", 3f, 0.1f, 30f, "Maximum distance in metres for ordinary voice and text-chat infection.");
			InfluenzaSpeechAngle = BindFloat(config, "Influenza", "SpeechAngleDegrees", 60f, 1f, 360f, "Full horizontal infection angle for voice and text chat. 60 means 30 degrees to each side.");
			InfluenzaSpeechChance = BindFloat(config, "Influenza", "SpeechChancePercent", 30f, 0f, 100f, "Infection chance for each eligible teammate per utterance or chat message. Zero prevents speech infection.");
			InfluenzaSpeechSilenceSeconds = BindFloat(config, "Influenza", "SpeechSilenceSeconds", 0.75f, 0.1f, 5f, "Silent gap required before voice counts as a new utterance. Text chat always uses one check per message.");
			InfluenzaSneezeNoiseRadius = BindFloat(config, "Influenza", "SneezeNoiseRadius", 5f, 0f, 100f, "Base distance in metres at which enemies hear sneezes, in every direction. Enemy hearing modifies this distance. Zero disables this enemy alert.");
			TwinsEnabled = RoleEnabled(config, "Twins");
			TwinsWeight = RoleWeight(config, "Twins");
			TwinsCarryStrengthMultiplier = BindFloat(config, "Twins", "CarryStrengthMultiplier", 1.5f, 1f, 3f, "Strength multiplier converted to temporary upgrade levels while both twins hold the same valuable. Rounded up to whole levels, capped at 200. Shop equipment is excluded.");
			TwinsCollisionReductionPercent = BindFloat(config, "Twins", "CollisionReductionPercent", 50f, 0f, 100f, "Percentage of collision value loss prevented during cooperative carrying.");
			TwinsRestRadius = BindFloat(config, "Twins", "RestRadius", 3f, 0.5f, 10f, "Maximum distance between twins while resting together.");
			TwinsRestHoldSeconds = BindFloat(config, "Twins", "RestHoldSeconds", 3f, 1f, 30f, "Seconds both twins must crouch and stay still without damage.");
			TwinsRestHealPercent = BindFloat(config, "Twins", "RestHealPercent", 10f, 0f, 100f, "Percentage of shared maximum HP restored by one completed rest.");
			TwinsRestCooldownSeconds = BindFloat(config, "Twins", "RestCooldownSeconds", 60f, 1f, 600f, "Pair-shared cooldown after successful rest. Interrupted rest spends nothing.");
			TwinsRendezvousStartRange = BindFloat(config, "Twins", "RendezvousStartRange", 15f, 1f, 100f, "Distance that enables reunion sprint assistance.");
			TwinsRendezvousEndRange = BindFloat(config, "Twins", "RendezvousEndRange", 5f, 0.5f, 100f, "Distance that ends reunion assistance. The smaller distance is always the end range.");
			TwinsRendezvousSpeedMultiplier = BindFloat(config, "Twins", "RendezvousSpeedMultiplier", 1.25f, 1f, 2f, "Sprint speed multiplier while moving toward a distant partner. Rounded up to a native Speed upgrade level.");
			TwinsDeliveryCarrySeconds = BindFloat(config, "Twins", "DeliveryCarrySeconds", 3f, 1f, 30f, "Continuous cooperative carrying outside delivery areas needed to qualify a valuable.");
			TwinsDeliveryRadius = BindFloat(config, "Twins", "DeliveryRadius", 5f, 0.5f, 20f, "Both twins must be within this distance of the valuable at delivery.");
			TwinsDeliveryBonusPercent = BindFloat(config, "Twins", "DeliveryBonusPercent", 10f, 0f, 100f, "Value bonus for a qualified valuable entering the truck or extraction area. Each valuable pays only once.");
			TwinsDeliveryStageLimit = BindInt(config, "Twins", "DeliveryStageLimit", 5000, 0, 10000, "Maximum total extra value for joint delivery per pair and stage.");
			PorterEnabled = RoleEnabled(config, "Porter");
			PorterWeight = RoleWeight(config, "Porter");
			PorterCapacity = BindFloat(config, "Porter", "Capacity", 15f, 1f, 100f, "Maximum combined weight of stored valuables. No item-count limit.");
			PorterHoldSeconds = BindFloat(config, "Porter", "HoldSeconds", 3f, 1f, 30f, "Continuous solo holding outside delivery areas required to store a valuable.");
			PorterUnloadSeconds = BindFloat(config, "Porter", "FullUnloadSeconds", 10f, 0f, 60f, "Seconds to unload a full load inside the truck or extraction area; scales with stored weight. Leaving the area or losing cargo to an enemy hit restarts the wait.");
			PorterDropOnEnemyHit = BindBool(config, "Porter", "DropOnEnemyHit", defaultValue: true, "Scatter all stored valuables on a confirmed enemy hit that reduces HP. Death releases all cargo.");
			PorterAllowedSizes = config.Bind<string>("Porter", "AllowedSizes", "Tiny,Small,Medium", "Comma-separated allowed valuable sizes: Tiny, Small, Medium, Big, Wide, Tall, VeryTall. Empty or unknown entries allow no additional sizes. Shop equipment is excluded.");
			DualWielderEnabled = RoleEnabled(config, "DualWielder");
			DualWielderWeight = RoleWeight(config, "DualWielder");
			DualWielderDelaySeconds = BindFloat(config, "DualWielder", "FollowDelaySeconds", 0.3f, 0.05f, 2f, "Seconds by which the second melee weapon follows the first. Battery and durability are shared; both weapons consume them on hit.");
			DualWielderLeftOffset = BindFloat(config, "DualWielder", "LeftOffset", 0.5f, 0.1f, 1.5f, "Distance in metres to the player's left for the second melee weapon.");
			RiggerEnemyDamageMultiplier = BindFloat(config, "Rigger", "EnemyDamageMultiplier", 2f, 1f, 10f, "Human Adventure Cable direct hit damage multiplier against enemies.");
			RiggerEnabled = RoleEnabled(config, "Rigger");
			RiggerWeight = RoleWeight(config, "Rigger");
			RiggerEnemyHoldMultiplier = BindFloat(config, "Rigger", "EnemyHoldDurationMultiplier", 1.5f, 1f, 3f, "Enemy restraint duration multiplier while using the Human Adventure Cable.");
			RiggerDurabilityMultiplier = BindFloat(config, "Rigger", "DurabilityConsumptionMultiplier", 0.5f, 0.1f, 1f, "Human Adventure Cable durability consumed on hits and throws. 0.5 means half the normal cost.");
			NegotiatorEnabled = RoleEnabled(config, "Negotiator");
			NegotiatorWeight = RoleWeight(config, "Negotiator");
			NegotiatorDeliveryCarryDistance = BindFloat(config, "Negotiator", "DeliveryCarryDistance", 20f, 1f, 200f, "Metres personally carried outside the truck before releasing a valuable at extraction. Applies to delivery checks; existing rights are kept. One delivery reward per stage, one separate kill reward, at most two rights.");
			NegotiatorDeliveryAllowedSizes = config.Bind<string>("Negotiator", "DeliveryAllowedSizes", "Big,Wide,Tall,VeryTall", "Comma-separated valuable sizes eligible for extraction delivery: Tiny, Small, Medium, Big, Wide, Tall, VeryTall. Names ignore case and surrounding spaces; unknown names are ignored. Blank disables delivery earnings. Truck delivery never qualifies.");
			NegotiatorEnemyTier = BindInt(config, "Negotiator", "EnemyTier", 3, 1, 3, "Exact native enemy tier required for a personally credited kill. Only this tier qualifies; Enhanced enemy reward bonuses do not change it. One kill reward per stage. Existing rights are kept.");
			SuperbotEnabled = BindBool(config, "???1", "Enabled", defaultValue: true, "???");
			DisasterEnabled = BindBool(config, "???2", "Enabled", defaultValue: true, "???");
		}

		internal int TruckUpgradeDrawWeight(string commandName)
		{
			if (!_truckUpgradeDrawWeights.TryGetValue(commandName, out ConfigEntry<int> value))
			{
				return 0;
			}
			return Math.Clamp(value.Value, 0, 1000);
		}

		internal bool RoleIsEnabled(StageRole role)
		{
			return RoleEnabledEntry(role)?.Value ?? false;
		}

		internal ConfigEntry<bool>? RoleEnabledEntry(StageRole role)
		{
			return (ConfigEntry<bool>?)(role switch
			{
				StageRole.Tank => TankEnabled, 
				StageRole.Runner => RunnerEnabled, 
				StageRole.Jumper => JumperEnabled, 
				StageRole.Lifter => LifterEnabled, 
				StageRole.Launcher => LauncherEnabled, 
				StageRole.Climber => ClimberEnabled, 
				StageRole.Flyer => FlyerEnabled, 
				StageRole.Tracker => TrackerEnabled, 
				StageRole.Ghost => GhostEnabled, 
				StageRole.Bomber => BomberEnabled, 
				StageRole.Medic => MedicEnabled, 
				StageRole.Phoenix => PhoenixEnabled, 
				StageRole.Jobless => JoblessEnabled, 
				StageRole.Rescuer => RescuerEnabled, 
				StageRole.Vampire => VampireEnabled, 
				StageRole.King => KingEnabled, 
				StageRole.Tuna => TunaEnabled, 
				StageRole.Musician => MusicianEnabled, 
				StageRole.Mage => MageEnabled, 
				StageRole.Gambler => GamblerEnabled, 
				StageRole.Hunter => HunterEnabled, 
				StageRole.Stinker => StinkerEnabled, 
				StageRole.Engineer => EngineerEnabled, 
				StageRole.Trickster => TricksterEnabled, 
				StageRole.Mechanic => MechanicEnabled, 
				StageRole.Electrician => ElectricianEnabled, 
				StageRole.Warden => WardenEnabled, 
				StageRole.Ninja => NinjaEnabled, 
				StageRole.Executioner => ExecutionerEnabled, 
				StageRole.Rider => RiderEnabled, 
				StageRole.Influencer => InfluencerEnabled, 
				StageRole.Werewolf => WerewolfEnabled, 
				StageRole.Berserker => BerserkerEnabled, 
				StageRole.Bodyguard => BodyguardEnabled, 
				StageRole.Rammer => RammerEnabled, 
				StageRole.Diver => DiverEnabled, 
				StageRole.Sniper => SniperEnabled, 
				StageRole.Imitator => ImitatorEnabled, 
				StageRole.Avenger => AvengerEnabled, 
				StageRole.Brawler => BrawlerEnabled, 
				StageRole.Influenza => InfluenzaEnabled, 
				StageRole.Twins => TwinsEnabled, 
				StageRole.Porter => PorterEnabled, 
				StageRole.DualWielder => DualWielderEnabled, 
				StageRole.Rigger => RiggerEnabled, 
				StageRole.Negotiator => NegotiatorEnabled, 
				StageRole.Superbot => SuperbotEnabled, 
				StageRole.Disaster => DisasterEnabled, 
				_ => null, 
			});
		}

		internal int RoleWeightValue(StageRole role)
		{
			return role switch
			{
				StageRole.Tank => TankWeight.Value, 
				StageRole.Runner => RunnerWeight.Value, 
				StageRole.Jumper => JumperWeight.Value, 
				StageRole.Lifter => LifterWeight.Value, 
				StageRole.Launcher => LauncherWeight.Value, 
				StageRole.Climber => ClimberWeight.Value, 
				StageRole.Flyer => FlyerWeight.Value, 
				StageRole.Tracker => TrackerWeight.Value, 
				StageRole.Ghost => GhostWeight.Value, 
				StageRole.Bomber => BomberWeight.Value, 
				StageRole.Medic => MedicWeight.Value, 
				StageRole.Phoenix => PhoenixWeight.Value, 
				StageRole.Jobless => JoblessWeight.Value, 
				StageRole.Rescuer => RescuerWeight.Value, 
				StageRole.Vampire => VampireWeight.Value, 
				StageRole.King => KingWeight.Value, 
				StageRole.Tuna => TunaWeight.Value, 
				StageRole.Musician => MusicianWeight.Value, 
				StageRole.Mage => MageWeight.Value, 
				StageRole.Gambler => GamblerWeight.Value, 
				StageRole.Hunter => HunterWeight.Value, 
				StageRole.Stinker => StinkerWeight.Value, 
				StageRole.Engineer => EngineerWeight.Value, 
				StageRole.Trickster => TricksterWeight.Value, 
				StageRole.Mechanic => MechanicWeight.Value, 
				StageRole.Electrician => ElectricianWeight.Value, 
				StageRole.Warden => WardenWeight.Value, 
				StageRole.Ninja => NinjaWeight.Value, 
				StageRole.Executioner => ExecutionerWeight.Value, 
				StageRole.Rider => RiderWeight.Value, 
				StageRole.Influencer => InfluencerWeight.Value, 
				StageRole.Werewolf => WerewolfWeight.Value, 
				StageRole.Berserker => BerserkerWeight.Value, 
				StageRole.Bodyguard => BodyguardWeight.Value, 
				StageRole.Rammer => RammerWeight.Value, 
				StageRole.Diver => DiverWeight.Value, 
				StageRole.Sniper => SniperWeight.Value, 
				StageRole.Imitator => ImitatorWeight.Value, 
				StageRole.Avenger => AvengerWeight.Value, 
				StageRole.Brawler => BrawlerWeight.Value, 
				StageRole.Influenza => InfluenzaWeight.Value, 
				StageRole.Twins => TwinsWeight.Value, 
				StageRole.Porter => PorterWeight.Value, 
				StageRole.DualWielder => DualWielderWeight.Value, 
				StageRole.Rigger => RiggerWeight.Value, 
				StageRole.Negotiator => NegotiatorWeight.Value, 
				StageRole.Superbot => 1, 
				StageRole.Disaster => 1, 
				_ => 0, 
			};
		}

		internal int ShowcaseMinimumForPlayerCount(int playerCount)
		{
			if (!ShowcaseGuaranteesEnabled.Value)
			{
				return 0;
			}
			return Mathf.Min(Mathf.Max(0, playerCount), PartySizeScalingValue(ShowcaseMinimumsByPlayerCount.Value, playerCount, allowZero: true));
		}

		internal int SupportMinimumForPlayerCount(int playerCount)
		{
			if (!SupportGuaranteesEnabled.Value)
			{
				return 0;
			}
			return Mathf.Min(Mathf.Max(0, playerCount), PartySizeScalingValue(SupportMinimumsByPlayerCount.Value, playerCount, allowZero: true));
		}

		internal int DangerRoleLimitForPlayerCount(int playerCount)
		{
			return PartySizeScalingValue(DangerRoleLimit.Value, playerCount);
		}

		internal int HardshipRoleLimitForPlayerCount(int playerCount)
		{
			return PartySizeScalingValue(HardshipRoleLimit.Value, playerCount);
		}

		private static int PartySizeScalingValue(string expression, int playerCount, bool allowZero = false)
		{
			RoleUpgradeScaling.TryParse(expression, 30, 30, out IReadOnlyList<UpgradeScalingRule> rules, out string _);
			int num = 0;
			foreach (UpgradeScalingRule item in rules)
			{
				if (playerCount >= item.Condition)
				{
					num = item.Level;
				}
			}
			return Mathf.Clamp(num, (!allowZero) ? 1 : 0, 30);
		}

		private static ConfigEntry<bool> RoleEnabled(ConfigFile config, string section)
		{
			return BindBool(config, section, "Enabled", defaultValue: true, "Includes this role in future random assignments. Turning it off does not remove an already assigned role.");
		}

		private static ConfigEntry<int> RoleWeight(ConfigFile config, string section, int defaultValue = 100)
		{
			return BindInt(config, section, "Weight", defaultValue, 0, 1000, "Relative random-assignment weight, not a percentage. Zero excludes this role from random draws; current assignments stay unchanged.");
		}

		private static ConfigEntry<string> PartySizeScaling(ConfigFile config, string section, string key, string defaultValue, string description)
		{
			return config.Bind<string>(section, key, defaultValue, description + " Use comma-separated party size:limit pairs; both numbers accept 0-30. Invalid pairs are ignored; the last duplicate party size wins. " + ((section == "Role Balance - Limits") ? "Effective limits are at least 1." : "Zero disables the minimum at that party size."));
		}

		private static ConfigEntry<string> ExpressionName(ConfigFile config, string section, string key, string defaultValue, string action)
		{
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Expected O, but got Unknown
			return config.Bind<string>(section, key, defaultValue, new ConfigDescription("Facial expression that triggers " + action + ".", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[7] { "Disabled", "Angry", "Sad", "Suspicious", "EyesClosed", "Scared", "Happy" }), Array.Empty<object>()));
		}

		private static ConfigEntry<int> UpgradeLevel(ConfigFile config, string section, string key, int defaultValue)
		{
			return BindInt(config, section, key, defaultValue, 0, 200, "Vanilla upgrade target level while this role is assigned.");
		}

		private static IReadOnlyDictionary<string, ConfigEntry<string>> UpgradeScalingEntries(ConfigFile config, string section, bool useHealthPercent, bool includeHealth)
		{
			Dictionary<string, ConfigEntry<string>> dictionary = new Dictionary<string, ConfigEntry<string>>(StringComparer.Ordinal);
			foreach (DynamicUpgradeDefinition definition in RoleUpgradeScaling.Definitions)
			{
				if (includeHealth || !(definition.Name == "Health"))
				{
					string text = (useHealthPercent ? RoleUpgradeScaling.BerserkerDefault(definition.Name) : RoleUpgradeScaling.InfluencerDefault(definition.Name));
					string text2 = (useHealthPercent ? "remaining HP percent at or below which the value applies" : "nearby-player count at or above which the value applies");
					dictionary[definition.Name] = config.Bind<string>(section, definition.Name + "UpgradeScaling", text, "Comma-separated condition:level pairs (" + text2 + "). " + $"Level range: 0-{definition.MaximumLevel}. Blank and conditions not yet reached use target level 0.");
				}
			}
			return dictionary;
		}

		private static ConfigEntry<string> BaseUpgradeScaling(ConfigFile config, string key, string defaultValue, int maximumLevel)
		{
			return config.Bind<string>("Base Upgrades", key, defaultValue, "Comma-separated level:value pairs. The last entry at or below the current run level sets the base vanilla upgrade target. " + $"Run level range: 1-999999. Value range: 0-{maximumLevel}; out-of-range numeric values are clamped. " + "Blank and levels not yet reached use target level 0.");
		}

		private void BindTruckUpgradeDrawWeight(ConfigFile config, string upgradeName)
		{
			_baseUpgradeDrawEnabled[upgradeName] = BindBool(config, "Base Upgrade Draw Selection", upgradeName, defaultValue: true, "Includes this upgrade in future truck draws, including All Upgrades results. Existing levels and configured base targets are kept. For AllUpgrades, controls the combined draw result only; individual upgrade switches still apply.");
			_truckUpgradeDrawWeights[upgradeName] = BindInt(config, "Base Upgrade Draw Weights", upgradeName, TruckDrawWeight.DefaultWeight(upgradeName), 0, 1000, "Relative selection weight for " + upgradeName + " in the truck Base Upgrade draw. " + ((upgradeName == "AllUpgrades") ? "All Upgrades uses this weight directly. " : "Weight gradually decreases toward CappedUpgradeWeightMultiplier as the upgrade approaches its draw maximum. Defaults reflect vanilla shop stock. ") + "Zero excludes it from direct selection.");
		}

		private static ConfigEntry<bool> BindBool(ConfigFile config, string section, string key, bool defaultValue, string description)
		{
			return config.Bind<bool>(section, key, defaultValue, description);
		}

		private static ConfigEntry<int> BindInt(ConfigFile config, string section, string key, int defaultValue, int minimum, int maximum, string description)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Expected O, but got Unknown
			return config.Bind<int>(section, key, defaultValue, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<int>(minimum, maximum), Array.Empty<object>()));
		}

		private static ConfigEntry<float> BindFloat(ConfigFile config, string section, string key, float defaultValue, float minimum, float maximum, string description)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Expected O, but got Unknown
			return config.Bind<float>(section, key, defaultValue, new ConfigDescription(description, (AcceptableValueBase)(object)new AcceptableValueRange<float>(minimum, maximum), Array.Empty<object>()));
		}
	}
	internal enum StageRole
	{
		Tank = 0,
		Runner = 1,
		Jumper = 2,
		Lifter = 3,
		Launcher = 4,
		Climber = 5,
		Flyer = 6,
		Tracker = 7,
		Ghost = 8,
		Bomber = 9,
		Medic = 10,
		Phoenix = 11,
		Jobless = 12,
		Rescuer = 13,
		Vampire = 14,
		King = 15,
		Tuna = 16,
		Musician = 17,
		Mage = 18,
		Gambler = 19,
		Hunter = 20,
		Stinker = 21,
		Engineer = 22,
		Trickster = 23,
		Mechanic = 24,
		Electrician = 25,
		Warden = 26,
		Ninja = 27,
		Executioner = 28,
		Rider = 29,
		Influencer = 30,
		Werewolf = 31,
		Berserker = 32,
		Bodyguard = 33,
		Rammer = 34,
		Diver = 35,
		Sniper = 36,
		Imitator = 37,
		Avenger = 38,
		Brawler = 39,
		Influenza = 40,
		Twins = 41,
		Porter = 42,
		DualWielder = 43,
		Rigger = 44,
		Negotiator = 45,
		Superbot = 1001,
		Disaster = 1002
	}
	internal static class RoleWireId
	{
		internal static int Encode(StageRole role)
		{
			return role switch
			{
				StageRole.Twins => 42, 
				StageRole.Porter => 43, 
				StageRole.DualWielder => 44, 
				StageRole.Rigger => 45, 
				StageRole.Negotiator => 46, 
				_ => (int)role, 
			};
		}

		internal static StageRole Decode(int value)
		{
			return value switch
			{
				41 => (StageRole)(-1), 
				42 => StageRole.Twins, 
				43 => StageRole.Porter, 
				44 => StageRole.DualWielder, 
				45 => StageRole.Rigger, 
				46 => StageRole.Negotiator, 
				_ => (StageRole)value, 
			};
		}
	}
	internal static class InfluenzaRules
	{
		internal const float IncubationSeconds = 30f;

		internal const int MaximumHealth = 75;

		internal const float SpeechSilenceSeconds = 0.75f;

		internal const float SneezeInvestigateRadius = 5f;

		internal static float SneezeInterval(double sample, float minimum = 30f, float maximum = 90f)
		{
			return Math.Min(minimum, maximum) + Math.Abs(maximum - minimum) * (float)Math.Clamp(sample, 0.0, 1.0);
		}

		internal static bool Infects(double sample, bool sneeze, double sneezePercent = 60.0, double speechPercent = 30.0)
		{
			double num = Math.Clamp(sneeze ? sneezePercent : speechPercent, 0.0, 100.0);
			if (sample >= 0.0 && sample <= 1.0)
			{
				if (num != 100.0)
				{
					return sample < num / 100.0;
				}
				return true;
			}
			return false;
		}

		internal static bool InRange(double distanceSquared, double horizontalDot, bool sneeze, double sneezeRange = 5.0, double speechRange = 3.0, double sneezeAngle = 40.0, double speechAngle = 60.0)
		{
			double num = (sneeze ? sneezeRange : speechRange);
			double num2 = (sneeze ? sneezeAngle : speechAngle) / 2.0;
			if (distanceSquared >= 0.0 && distanceSquared <= num * num)
			{
				return horizontalDot + 1E-06 >= Math.Cos(num2 * Math.PI / 180.0);
			}
			return false;
		}
	}
	internal sealed class InfluenzaState
	{
		private int _normalMaximumHealth;

		private int _healthUpgrade;

		private float _lastVoiceAt = float.NegativeInfinity;

		internal float OnsetAt { get; }

		internal float NextSneezeAt { get; set; } = float.PositiveInfinity;

		internal bool SymptomsStarted { get; set; }

		internal bool HealthCaptured { get; private set; }

		internal InfluenzaState(float infectedAt, float incubationSeconds = 30f)
		{
			OnsetAt = infectedAt + incubationSeconds;
		}

		internal bool Symptomatic(float now)
		{
			return now >= OnsetAt;
		}

		internal bool ObserveVoice(float now, bool audible, float silenceSeconds = 0.75f)
		{
			if (!audible)
			{
				return false;
			}
			bool num = now - _lastVoiceAt >= silenceSeconds;
			_lastVoiceAt = now;
			if (num)
			{
				return Symptomatic(now);
			}
			return false;
		}

		internal void CaptureHealth(int maximum, int healthUpgrade)
		{
			if (!HealthCaptured)
			{
				_normalMaximumHealth = maximum;
				_healthUpgrade = healthUpgrade;
				HealthCaptured = true;
			}
		}

		internal int RestoredMaximum(int healthUpgrade)
		{
			return Math.Max(1, _normalMaximumHealth + 20 * (healthUpgrade - _healthUpgrade));
		}
	}
	internal sealed class StageRoleController : MonoBehaviour
	{
		private sealed class TwinPair
		{
			internal readonly RoleAssignment[] Members = new RoleAssignment[2] { first, second };

			internal readonly PlayerAvatar[] Players = (PlayerAvatar[])(object)new PlayerAvatar[2] { first.Player, second.Player };

			internal readonly int[] Maximum = new int[2];

			internal readonly int[] PendingUpgradeHealing = new int[2];

			internal readonly int[] SpeedBonus = new int[2];

			internal readonly Vector3[] Positions = (Vector3[])(object)new Vector3[2]
			{
				((Component)first.Player).transform.position,
				((Component)second.Player).transform