Decompiled source of RollOfTheScoutPerk v1.0.8

com.peakmodding.RollOfTheScout.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Peak;
using Peak.Afflictions;
using PeakNativeUi;
using PeakOverheadCompat;
using Photon.Pun;
using Photon.Realtime;
using RollOfTheScout.Patches;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Zorro.Core;

[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("com.peakmodding.RollOfTheScout")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("com.peakmodding.RollOfTheScout")]
[assembly: AssemblyTitle("RollOfTheScout")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 PeakNativeUi
{
	public static class NativeFont
	{
		private static TMP_FontAsset _resolved;

		public static TMP_FontAsset Resolve()
		{
			if ((Object)(object)_resolved != (Object)null)
			{
				return _resolved;
			}
			try
			{
				FontFallbackSwapper instance = FontFallbackSwapper.instance;
				if ((Object)(object)instance != (Object)null && (Object)(object)instance.mainBaseFont != (Object)null && HasCjk(instance.mainBaseFont))
				{
					return _resolved = instance.mainBaseFont;
				}
			}
			catch
			{
			}
			try
			{
				PlayerName[] array = Resources.FindObjectsOfTypeAll<PlayerName>();
				foreach (PlayerName val in array)
				{
					if (!((Object)(object)val == (Object)null) && !((Object)(object)val.text == (Object)null) && HasCjk(((TMP_Text)val.text).font))
					{
						return _resolved = ((TMP_Text)val.text).font;
					}
				}
			}
			catch
			{
			}
			TMP_FontAsset val2 = null;
			TMP_FontAsset val3 = null;
			try
			{
				TMP_FontAsset[] array2 = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
				foreach (TMP_FontAsset val4 in array2)
				{
					if (!((Object)(object)val4 == (Object)null))
					{
						if ((Object)(object)val2 == (Object)null && ((Object)val4).name != null && ((Object)val4).name.IndexOf("Daruma", StringComparison.OrdinalIgnoreCase) >= 0)
						{
							val2 = val4;
						}
						if ((Object)(object)val3 == (Object)null && HasCjk(val4))
						{
							val3 = val4;
						}
						if ((Object)(object)val2 != (Object)null && (Object)(object)val3 != (Object)null)
						{
							break;
						}
					}
				}
			}
			catch
			{
			}
			if ((Object)(object)val2 != (Object)null && HasCjk(val2))
			{
				return _resolved = val2;
			}
			if ((Object)(object)val3 != (Object)null)
			{
				return _resolved = val3;
			}
			try
			{
				TMP_FontAsset defaultFontAsset = TMP_Settings.defaultFontAsset;
				if ((Object)(object)defaultFontAsset != (Object)null && HasCjk(defaultFontAsset))
				{
					return _resolved = defaultFontAsset;
				}
			}
			catch
			{
			}
			return null;
		}

		private static bool HasCjk(TMP_FontAsset font)
		{
			try
			{
				return (Object)(object)font != (Object)null && font.HasCharacter('职', true, false);
			}
			catch
			{
				return false;
			}
		}
	}
	public static class NativeOverlay
	{
		private sealed class Block
		{
			public RectTransform Root;

			public Image Bg;

			public TextMeshProUGUI Text;

			public float WidthFraction;

			public bool Bold;

			public string Last;

			public float LastSize;

			public int LastScreenHeight;

			public int LastVersion = -1;
		}

		private static bool _ready;

		private static float _nextTry;

		private static GameObject _root;

		private static Block _toast;

		private static Block _panel;

		private static Block _intro;

		public static bool Ready
		{
			get
			{
				if (_ready && (Object)(object)_root == (Object)null)
				{
					_ready = false;
				}
				return _ready;
			}
		}

		public static bool Ensure()
		{
			if (Ready)
			{
				return true;
			}
			if (Time.time < _nextTry)
			{
				return false;
			}
			_nextTry = Time.time + 1f;
			try
			{
				TMP_FontAsset val = NativeFont.Resolve();
				if ((Object)(object)val == (Object)null)
				{
					return false;
				}
				Build(val);
				_ready = true;
				return true;
			}
			catch (Exception ex)
			{
				Debug.LogWarning((object)("[PeakNativeUi] 创建原生字体叠加层失败(将回退旧绘制):" + ex.Message));
				return false;
			}
		}

		public static void SetToast(IList<string> lines)
		{
			if (Ready)
			{
				SetBlock(_toast, Join(lines, null), _toast.Bold);
			}
		}

		public static void SetToast(IList<string> lines, int version)
		{
			if (Ready)
			{
				Block toast = _toast;
				if (toast != null && (version < 0 || version != toast.LastVersion))
				{
					toast.LastVersion = version;
					SetBlock(toast, Join(lines, null), toast.Bold);
				}
			}
		}

		public static void SetPanel(string title, IList<string> lines, bool visible)
		{
			SetPanel(title, lines, visible, -1);
		}

		public static void SetPanel(string title, IList<string> lines, bool visible, int version)
		{
			if (Ready)
			{
				Block panel = _panel;
				if (!visible)
				{
					SetBlock(panel, null, panel.Bold);
					panel.LastVersion = int.MinValue;
					SetBlock(_intro, null, _intro.Bold);
					_intro.LastVersion = int.MinValue;
				}
				else if (version < 0 || version != panel.LastVersion)
				{
					panel.LastVersion = version;
					SetBlock(panel, Join(lines, title), panel.Bold);
				}
			}
		}

		public static void SetPanelHidden()
		{
			if (Ready)
			{
				SetBlock(_panel, null, _panel.Bold);
				_panel.LastVersion = int.MinValue;
				SetBlock(_intro, null, _intro.Bold);
				_intro.LastVersion = int.MinValue;
			}
		}

		public static void SetClassIntro(string title, IList<string> lines, bool visible)
		{
			SetClassIntro(title, lines, visible, -1);
		}

		public static void SetClassIntro(string title, IList<string> lines, bool visible, int version)
		{
			if (!Ready)
			{
				return;
			}
			Block intro = _intro;
			if (!visible)
			{
				SetBlock(intro, null, intro.Bold);
				intro.LastVersion = int.MinValue;
				return;
			}
			if (version < 0 || version != intro.LastVersion)
			{
				intro.LastVersion = version;
				SetBlock(intro, Join(lines, title), intro.Bold);
			}
			PositionIntro();
		}

		public static void SetClassIntroHidden()
		{
			if (Ready)
			{
				SetBlock(_intro, null, _intro.Bold);
				_intro.LastVersion = int.MinValue;
			}
		}

		private static void PositionIntro()
		{
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			Block intro = _intro;
			if (intro != null && !((Object)(object)intro.Root == (Object)null) && ((Component)intro.Root).gameObject.activeSelf)
			{
				float num = 0f;
				if (_panel != null && (Object)(object)_panel.Root != (Object)null && ((Component)_panel.Root).gameObject.activeSelf)
				{
					num = _panel.Root.sizeDelta.y;
				}
				intro.Root.anchoredPosition = new Vector2(-12f, 0f - (12f + num + 8f));
			}
		}

		public static void Dispose()
		{
			try
			{
				if ((Object)(object)_root != (Object)null)
				{
					Object.Destroy((Object)(object)_root);
				}
			}
			catch
			{
			}
			_root = null;
			_toast = null;
			_panel = null;
			_intro = null;
			_ready = false;
			_nextTry = 0f;
		}

		private static void EnsureCjkFallback(TMP_FontAsset font)
		{
			if ((Object)(object)font == (Object)null)
			{
				return;
			}
			try
			{
				if (font.fallbackFontAssetTable == null)
				{
					font.fallbackFontAssetTable = new List<TMP_FontAsset>();
				}
				foreach (TMP_FontAsset item in font.fallbackFontAssetTable)
				{
					if ((Object)(object)item != (Object)null && ((Object)item).name == "RollOfTheScout.CJK")
					{
						return;
					}
				}
				string text = "C:\\Windows\\Fonts\\simhei.ttf";
				if (!File.Exists(text))
				{
					text = "C:\\Windows\\Fonts\\msyh.ttc";
				}
				if (!File.Exists(text))
				{
					return;
				}
				MethodInfo method = typeof(TMP_FontAsset).GetMethod("CreateFontAsset", new Type[1] { typeof(string) });
				if (!(method == null))
				{
					object? obj = method.Invoke(null, new object[1] { text });
					TMP_FontAsset val = (TMP_FontAsset)((obj is TMP_FontAsset) ? obj : null);
					if (!((Object)(object)val == (Object)null))
					{
						((Object)val).name = "RollOfTheScout.CJK";
						font.fallbackFontAssetTable.Add(val);
					}
				}
			}
			catch
			{
			}
		}

		private static void Build(TMP_FontAsset font)
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected O, but got Unknown
			EnsureCjkFallback(font);
			_root = new GameObject("RollOfTheScout.NativeOverlay", new Type[1] { typeof(RectTransform) });
			Object.DontDestroyOnLoad((Object)(object)_root);
			Canvas obj = _root.AddComponent<Canvas>();
			obj.renderMode = (RenderMode)0;
			obj.sortingOrder = 32000;
			_toast = CreateBlock("Toast", font, rightSide: false, 0.45f, bold: true);
			_panel = CreateBlock("ClassPanel", font, rightSide: true, 0.44f, bold: false);
			_intro = CreateBlock("ClassIntro", font, rightSide: true, 0.4f, bold: false);
		}

		private static Block CreateBlock(string name, TMP_FontAsset font, bool rightSide, float widthFraction, bool bold)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Expected O, but got Unknown
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0128: Unknown result type (might be due to invalid IL or missing references)
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0141: Unknown result type (might be due to invalid IL or missing references)
			//IL_0155: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) });
			val.transform.SetParent(_root.transform, false);
			RectTransform component = val.GetComponent<RectTransform>();
			float num = (rightSide ? 1f : 0f);
			Vector2 val2 = default(Vector2);
			((Vector2)(ref val2))..ctor(num, 1f);
			component.anchorMax = val2;
			component.anchorMin = val2;
			component.pivot = new Vector2(num, 1f);
			component.anchoredPosition = (rightSide ? new Vector2(-12f, -12f) : new Vector2(12f, -12f));
			component.sizeDelta = new Vector2(400f, 40f);
			Image val3 = val.AddComponent<Image>();
			((Graphic)val3).color = new Color(0f, 0f, 0f, 0.6f);
			((Graphic)val3).raycastTarget = false;
			GameObject val4 = new GameObject("Text", new Type[1] { typeof(RectTransform) });
			val4.transform.SetParent(val.transform, false);
			RectTransform component2 = val4.GetComponent<RectTransform>();
			((Vector2)(ref val2))..ctor(0f, 1f);
			component2.anchorMax = val2;
			component2.anchorMin = val2;
			component2.pivot = new Vector2(0f, 1f);
			component2.anchoredPosition = new Vector2(10f, -6f);
			TextMeshProUGUI val5 = val4.AddComponent<TextMeshProUGUI>();
			((TMP_Text)val5).font = font;
			if ((Object)(object)((TMP_Asset)font).material != (Object)null)
			{
				((TMP_Text)val5).fontSharedMaterial = ((TMP_Asset)font).material;
			}
			((Graphic)val5).raycastTarget = false;
			((TMP_Text)val5).alignment = (TextAlignmentOptions)257;
			((TMP_Text)val5).textWrappingMode = (TextWrappingModes)1;
			((TMP_Text)val5).overflowMode = (TextOverflowModes)0;
			try
			{
				PropertyInfo property = typeof(TextMeshProUGUI).GetProperty("enableWordWrapping");
				if (property != null && property.CanWrite)
				{
					property.SetValue(val5, true);
				}
			}
			catch
			{
			}
			((Graphic)val5).color = Color.white;
			((TMP_Text)val5).fontStyle = (FontStyles)(bold ? 1 : 0);
			return new Block
			{
				Root = component,
				Bg = val3,
				Text = val5,
				WidthFraction = widthFraction,
				Bold = bold
			};
		}

		private static void SetBlock(Block b, string content, bool bold)
		{
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Unknown result type (might be due to invalid IL or missing references)
			//IL_012d: Unknown result type (might be due to invalid IL or missing references)
			if (b == null || (Object)(object)b.Root == (Object)null)
			{
				return;
			}
			if (string.IsNullOrEmpty(content))
			{
				if (((Component)b.Root).gameObject.activeSelf)
				{
					((Component)b.Root).gameObject.SetActive(false);
				}
				b.Last = null;
				return;
			}
			if (!((Component)b.Root).gameObject.activeSelf)
			{
				((Component)b.Root).gameObject.SetActive(true);
			}
			float num = CurrentFontSize();
			if (!(content == b.Last) || !Mathf.Approximately(num, b.LastSize) || Screen.height != b.LastScreenHeight)
			{
				b.Last = content;
				b.LastSize = num;
				b.LastScreenHeight = Screen.height;
				float num2 = (float)Screen.width * b.WidthFraction;
				TextMeshProUGUI text = b.Text;
				((TMP_Text)text).fontSize = num;
				((TMP_Text)text).fontStyle = (FontStyles)(bold ? 1 : 0);
				((TMP_Text)text).text = content;
				RectTransform rectTransform = ((TMP_Text)text).rectTransform;
				rectTransform.sizeDelta = new Vector2(num2 - 20f, 4000f);
				((TMP_Text)text).ForceMeshUpdate(false, false);
				float preferredHeight = ((TMP_Text)text).preferredHeight;
				rectTransform.sizeDelta = new Vector2(num2 - 20f, preferredHeight);
				b.Root.sizeDelta = new Vector2(num2, preferredHeight + 12f);
			}
		}

		private static float CurrentFontSize()
		{
			return Mathf.Max(16, Mathf.RoundToInt((float)Screen.height / 48f));
		}

		private static string Join(IList<string> lines, string title)
		{
			if ((lines == null || lines.Count == 0) && string.IsNullOrEmpty(title))
			{
				return null;
			}
			StringBuilder stringBuilder = new StringBuilder();
			if (!string.IsNullOrEmpty(title))
			{
				stringBuilder.Append("<color=#FFE666><b>").Append(title).Append("</b></color>");
			}
			if (lines != null)
			{
				for (int i = 0; i < lines.Count; i++)
				{
					if (!string.IsNullOrEmpty(lines[i]))
					{
						if (stringBuilder.Length > 0)
						{
							stringBuilder.Append('\n');
						}
						stringBuilder.Append(lines[i]);
					}
				}
			}
			if (stringBuilder.Length != 0)
			{
				return stringBuilder.ToString();
			}
			return null;
		}
	}
}
namespace PeakOverheadCompat
{
	public static class ThirdPartyOverheadNameCompat
	{
		internal static class BpdExtractNamePatch
		{
			public static void Postfix(string objName, ref string __result)
			{
				try
				{
					if (Active && !string.IsNullOrEmpty(__result) && TryActorFromObjName(objName, out var actor) && !string.IsNullOrEmpty(ClassNameOf(actor)))
					{
						__result = Rebuild(__result, actor);
					}
				}
				catch
				{
				}
			}
		}

		internal static class BpdLabelPatch
		{
			private sealed class LabelState
			{
				public string Original;

				public string Desired;
			}

			private static readonly ConditionalWeakTable<object, LabelState> States = new ConditionalWeakTable<object, LabelState>();

			public static void Postfix(object __instance)
			{
				try
				{
					if (__instance == null || _bpdDisplayNameField == null || _bpdCharacterField == null || _bpdLastContentField == null)
					{
						return;
					}
					object? value = _bpdCharacterField.GetValue(__instance);
					Character val = (Character)((value is Character) ? value : null);
					if (!((Object)(object)val == (Object)null) && !((Object)(object)((MonoBehaviourPun)val).photonView == (Object)null) && ((MonoBehaviourPun)val).photonView.Owner != null)
					{
						int actorNumber = ((MonoBehaviourPun)val).photonView.Owner.ActorNumber;
						LabelState orCreateValue = States.GetOrCreateValue(__instance);
						if (orCreateValue.Original == null)
						{
							orCreateValue.Original = Strip(_bpdDisplayNameField.GetValue(__instance) as string);
						}
						string text = (string.IsNullOrEmpty(orCreateValue.Original) ? orCreateValue.Original : Rebuild(orCreateValue.Original, actorNumber));
						if (!(orCreateValue.Desired == text))
						{
							orCreateValue.Desired = text;
							_bpdDisplayNameField.SetValue(__instance, text);
							_bpdLastContentField.SetValue(__instance, null);
						}
					}
				}
				catch
				{
				}
			}
		}

		internal static class SodRefreshPatch
		{
			public static void Prefix(object __instance, ref string name)
			{
				try
				{
					if (__instance == null || string.IsNullOrEmpty(name) || !Active || _sodEntriesField == null || _sodEntryLabelField == null || (_sodInstanceProp == null && _sodInstanceField == null))
					{
						return;
					}
					object obj = ((_sodInstanceProp != null) ? _sodInstanceProp.GetValue(null) : _sodInstanceField.GetValue(null));
					if (obj == null || !(_sodEntriesField.GetValue(obj) is IDictionary dictionary))
					{
						return;
					}
					foreach (DictionaryEntry item in dictionary)
					{
						if (_sodEntryLabelField.GetValue(item.Value) == __instance)
						{
							object key = item.Key;
							Character val = (Character)((key is Character) ? key : null);
							if (val != null && !((Object)(object)((MonoBehaviourPun)val).photonView == (Object)null) && ((MonoBehaviourPun)val).photonView.Owner != null)
							{
								name = Rebuild(name, ((MonoBehaviourPun)val).photonView.Owner.ActorNumber);
							}
							break;
						}
					}
				}
				catch
				{
				}
			}
		}

		private const string BpdDll = "BetterPlayerDistance.dll";

		private const string SodDll = "SenseOfDirection.dll";

		private static Func<int, string> _classNameByActor;

		private static Action<string> _warn;

		private static bool _initialized;

		private static readonly Dictionary<int, string> _colorByViewId = new Dictionary<int, string>();

		private static readonly Dictionary<int, Character> _characterByActor = new Dictionary<int, Character>();

		private static float _nextTry;

		private static bool _bpdDone;

		private static bool _sodDone;

		private static bool _bpdApplied;

		private static bool _sodApplied;

		private static bool _loggedApplied;

		private static FieldInfo _bpdCharacterField;

		private static FieldInfo _bpdDisplayNameField;

		private static FieldInfo _bpdLastContentField;

		private static FieldInfo _sodEntriesField;

		private static FieldInfo _sodEntryLabelField;

		private static PropertyInfo _sodInstanceProp;

		private static FieldInfo _sodInstanceField;

		private const string ColorFallback = "FFFFFF";

		private const float ColorRetrySeconds = 0.5f;

		private static readonly Dictionary<int, float> _colorRetryAt = new Dictionary<int, float>();

		internal static bool Active
		{
			get
			{
				if (_initialized)
				{
					return _classNameByActor != null;
				}
				return false;
			}
		}

		public static void Initialize(Func<int, string> classNameByActor, Action<string> warn)
		{
			_classNameByActor = classNameByActor;
			_warn = warn;
			_initialized = true;
		}

		public static void Tick(Harmony harmony)
		{
			if (_initialized && harmony != null && (!_bpdDone || !_sodDone) && !(Time.time < _nextTry))
			{
				_nextTry = Time.time + 1f;
				if (!_bpdDone)
				{
					_bpdDone = !Installed("BetterPlayerDistance.dll") || ApplyBpd(harmony);
				}
				if (!_sodDone)
				{
					_sodDone = !Installed("SenseOfDirection.dll") || ApplySod(harmony);
				}
				if (_bpdApplied && _sodApplied && !_loggedApplied)
				{
					_loggedApplied = true;
					Warn("[兼容] 已挂载第三方头顶id显示模组兼容补丁(BetterPlayerDistance / Sense of Direction)。");
				}
			}
		}

		private static bool Installed(string dllFileName)
		{
			try
			{
				string pluginPath = Paths.PluginPath;
				if (!string.IsNullOrEmpty(pluginPath) && Directory.Exists(pluginPath))
				{
					return Directory.GetFiles(pluginPath, dllFileName, SearchOption.AllDirectories).Length != 0;
				}
			}
			catch
			{
			}
			return false;
		}

		private static void Warn(string msg)
		{
			try
			{
				_warn?.Invoke(msg);
			}
			catch
			{
			}
		}

		private static Type FindType(string assemblyName, string simpleName)
		{
			try
			{
				Assembly assembly = FindAssembly(assemblyName);
				if (assembly != null)
				{
					Type[] types = assembly.GetTypes();
					foreach (Type type in types)
					{
						if (type != null && type.Name == simpleName)
						{
							return type;
						}
					}
				}
			}
			catch
			{
			}
			return null;
		}

		private static Assembly FindAssembly(string simpleName)
		{
			try
			{
				Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
				foreach (Assembly assembly in assemblies)
				{
					if (assembly != null && assembly.GetName().Name == simpleName)
					{
						return assembly;
					}
				}
			}
			catch
			{
			}
			return null;
		}

		private static bool ApplyBpd(Harmony harmony)
		{
			//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Expected O, but got Unknown
			//IL_010a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0117: Expected O, but got Unknown
			try
			{
				Type type = FindType("BetterPlayerDistance", "Tracking");
				Type type2 = FindType("BetterPlayerDistance", "PlayerLabel");
				if (type == null || type2 == null)
				{
					return false;
				}
				MethodInfo method = type.GetMethod("ExtractDisplayName", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				MethodInfo method2 = type2.GetMethod("UpdateState", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				_bpdCharacterField = type2.GetField("_character", BindingFlags.Instance | BindingFlags.NonPublic);
				_bpdDisplayNameField = type2.GetField("_displayName", BindingFlags.Instance | BindingFlags.NonPublic);
				_bpdLastContentField = type2.GetField("_lastContent", BindingFlags.Instance | BindingFlags.NonPublic);
				if (method == null || method2 == null || _bpdCharacterField == null || _bpdDisplayNameField == null || _bpdLastContentField == null)
				{
					return false;
				}
				harmony.Patch((MethodBase)method, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(BpdExtractNamePatch), "Postfix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				harmony.Patch((MethodBase)method2, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(BpdLabelPatch), "Postfix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_bpdApplied = true;
				return true;
			}
			catch (Exception ex)
			{
				Warn("[兼容] BetterPlayerDistance 补丁失败:" + ex.Message);
				return true;
			}
		}

		private static bool ApplySod(Harmony harmony)
		{
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Expected O, but got Unknown
			try
			{
				Type type = FindType("SenseOfDirection", "PlayerLabelController");
				Type type2 = FindType("SenseOfDirection", "PlayerLabel");
				if (type == null || type2 == null)
				{
					return false;
				}
				_sodEntriesField = type.GetField("_entries", BindingFlags.Instance | BindingFlags.NonPublic);
				_sodInstanceProp = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				_sodInstanceField = ((_sodInstanceProp == null) ? type.GetField("_instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) : null);
				_sodEntryLabelField = type.GetNestedType("Entry", BindingFlags.NonPublic)?.GetField("Label", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				MethodInfo methodInfo = null;
				MethodInfo[] methods = type2.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (MethodInfo methodInfo2 in methods)
				{
					if (!(methodInfo2.Name != "Refresh"))
					{
						ParameterInfo[] parameters = methodInfo2.GetParameters();
						if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(string))
						{
							methodInfo = methodInfo2;
							break;
						}
					}
				}
				if (methodInfo == null || _sodEntriesField == null || (_sodInstanceProp == null && _sodInstanceField == null) || _sodEntryLabelField == null)
				{
					return false;
				}
				harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(SodRefreshPatch), "Prefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_sodApplied = true;
				return true;
			}
			catch (Exception ex)
			{
				Warn("[兼容] Sense of Direction 补丁失败:" + ex.Message);
				return true;
			}
		}

		internal static string ClassNameOf(int actor)
		{
			if (_classNameByActor == null)
			{
				return null;
			}
			try
			{
				return _classNameByActor(actor);
			}
			catch
			{
				return null;
			}
		}

		internal static Character CharacterOf(int actor)
		{
			if (_characterByActor.TryGetValue(actor, out var value) && (Object)(object)value != (Object)null)
			{
				return value;
			}
			List<Character> allCharacters = Character.AllCharacters;
			if (allCharacters != null)
			{
				for (int i = 0; i < allCharacters.Count; i++)
				{
					Character val = allCharacters[i];
					if (!((Object)(object)val == (Object)null) && !((Object)(object)((MonoBehaviourPun)val).photonView == (Object)null) && ((MonoBehaviourPun)val).photonView.Owner != null && ((MonoBehaviourPun)val).photonView.Owner.ActorNumber == actor)
					{
						_characterByActor[actor] = val;
						return val;
					}
				}
			}
			return null;
		}

		internal static string ColorHexOf(int actor)
		{
			Character val = CharacterOf(actor);
			int num = (((Object)(object)val != (Object)null && (Object)(object)((MonoBehaviourPun)val).photonView != (Object)null) ? ((MonoBehaviourPun)val).photonView.ViewID : 0);
			if (num != 0 && _colorByViewId.TryGetValue(num, out var value))
			{
				return value;
			}
			if (_colorRetryAt.TryGetValue(actor, out var value2) && Time.time < value2)
			{
				return "FFFFFF";
			}
			if (TryReadColorOf(val, out var hex))
			{
				if (num != 0)
				{
					_colorByViewId[num] = hex;
				}
				return hex;
			}
			_colorRetryAt[actor] = Time.time + 0.5f;
			return "FFFFFF";
		}

		private static bool TryReadColorOf(Character ch, out string hex)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			hex = "FFFFFF";
			try
			{
				if ((Object)(object)ch == (Object)null || ch.refs == null || (Object)(object)ch.refs.customization == (Object)null)
				{
					return false;
				}
				hex = ColorUtility.ToHtmlStringRGB(ch.refs.customization.PlayerColor);
				return true;
			}
			catch
			{
				return false;
			}
		}

		internal static string BuildPrefix(int actor)
		{
			if (!Active)
			{
				return null;
			}
			string text = ClassNameOf(actor);
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			return "<color=#" + ColorHexOf(actor) + ">【" + text + "】</color>";
		}

		internal static string Strip(string text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return text;
			}
			if (!text.StartsWith("<color=#", StringComparison.Ordinal))
			{
				return text;
			}
			int num = text.IndexOf("</color>", StringComparison.Ordinal);
			if (num < 0)
			{
				return text;
			}
			int num2 = text.IndexOf('】');
			if (num2 < 0 || num2 > num)
			{
				return text;
			}
			return text.Substring(num + "</color>".Length);
		}

		internal static string Rebuild(string text, int actor)
		{
			string text2 = Strip(text);
			string text3 = BuildPrefix(actor);
			if (text3 != null)
			{
				return text3 + text2;
			}
			return text2;
		}

		internal static bool TryActorFromObjName(string objName, out int actor)
		{
			actor = 0;
			if (string.IsNullOrEmpty(objName))
			{
				return false;
			}
			int num = objName.IndexOf('[');
			int num2 = objName.LastIndexOf(':');
			if (num == -1 || num2 == -1 || num2 <= num)
			{
				return false;
			}
			return int.TryParse(objName.Substring(num2 + 1).Trim().TrimEnd(']')
				.Trim(), out actor);
		}
	}
}
namespace RollOfTheScout
{
	public static class ClassDisplayNet
	{
		public const string ModId = "com.peakmodding.RollOfTheScout";

		public const byte EventCode = 197;

		public const byte KindClassSnapshot = 1;

		public const byte KindToast = 2;

		public const byte KindClassNames = 3;

		public const byte KindClassKeywords = 4;

		public const byte KindClassIntro = 5;

		public const byte KindPerkSources = 6;

		public const float ClassTextResendSeconds = 30f;

		public const float SnapshotIntervalSeconds = 2f;

		public static void BroadcastClassSnapshot(IList<KeyValuePair<int, int>> actorClassPairs)
		{
			if (!ShouldBroadcast() || actorClassPairs == null || actorClassPairs.Count == 0)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < actorClassPairs.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(';');
				}
				stringBuilder.Append(actorClassPairs[i].Key).Append('=').Append(actorClassPairs[i].Value);
			}
			Send(1, stringBuilder.ToString());
		}

		public static void BroadcastClassNames(IList<KeyValuePair<int, string>> codeToName)
		{
			if (!ShouldBroadcast() || codeToName == null || codeToName.Count == 0)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < codeToName.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(';');
				}
				stringBuilder.Append(codeToName[i].Key).Append('=').Append(Sanitize(codeToName[i].Value));
			}
			Send(3, stringBuilder.ToString());
		}

		public static void BroadcastClassKeywords(IList<KeyValuePair<int, string>> codeToKeywords)
		{
			SendTextMap(4, codeToKeywords);
		}

		public static void BroadcastClassIntro(IList<KeyValuePair<int, string>> codeToIntro)
		{
			SendTextMap(5, codeToIntro);
		}

		public static void BroadcastPerkSources(IList<KeyValuePair<int, string>> actorToSources)
		{
			if (actorToSources == null || actorToSources.Count == 0)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			foreach (KeyValuePair<int, string> actorToSource in actorToSources)
			{
				if (!string.IsNullOrEmpty(actorToSource.Value))
				{
					if (stringBuilder.Length > 0)
					{
						stringBuilder.Append(';');
					}
					stringBuilder.Append(actorToSource.Key).Append('=').Append(actorToSource.Value);
				}
			}
			if (stringBuilder.Length != 0)
			{
				Send(6, stringBuilder.ToString());
			}
		}

		private static void SendTextMap(byte kind, IList<KeyValuePair<int, string>> pairs)
		{
			if (!ShouldBroadcast() || pairs == null || pairs.Count == 0)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < pairs.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(';');
				}
				stringBuilder.Append(pairs[i].Key).Append('=').Append(Sanitize(pairs[i].Value));
			}
			Send(kind, stringBuilder.ToString());
		}

		private static string Sanitize(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return string.Empty;
			}
			return name.Replace(';', '_').Replace('=', '_').Replace('\n', ' ')
				.Replace('\r', ' ');
		}

		public static void BroadcastToast(string text)
		{
			if (ShouldBroadcast() && !string.IsNullOrEmpty(text))
			{
				Send(2, text);
			}
		}

		private static bool ShouldBroadcast()
		{
			if (!ModConfig.General.DisplaySync.Value)
			{
				return false;
			}
			if (!PhotonNetwork.InRoom)
			{
				return false;
			}
			return PhotonNetwork.IsMasterClient;
		}

		private static void Send(byte kind, string data)
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Expected O, but got Unknown
			try
			{
				object[] array = new object[3] { "com.peakmodding.RollOfTheScout", kind, data };
				PhotonNetwork.RaiseEvent((byte)197, (object)array, new RaiseEventOptions
				{
					Receivers = (ReceiverGroup)0
				}, SendOptions.SendReliable);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"[RollOfTheScout] 职业显示网络广播失败(kind={kind}):{ex.Message}");
			}
		}

		public static List<KeyValuePair<int, int>> ParseSnapshot(string data)
		{
			List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>();
			if (string.IsNullOrEmpty(data))
			{
				return list;
			}
			string[] array = data.Split(';');
			foreach (string text in array)
			{
				if (!string.IsNullOrEmpty(text))
				{
					int num = text.IndexOf('=');
					if (num > 0 && int.TryParse(text.Substring(0, num), out var result) && int.TryParse(text.Substring(num + 1), out var result2))
					{
						list.Add(new KeyValuePair<int, int>(result, result2));
					}
				}
			}
			return list;
		}

		public static List<KeyValuePair<int, string>> ParseClassNames(string data)
		{
			List<KeyValuePair<int, string>> list = new List<KeyValuePair<int, string>>();
			if (string.IsNullOrEmpty(data))
			{
				return list;
			}
			string[] array = data.Split(';');
			foreach (string text in array)
			{
				if (!string.IsNullOrEmpty(text))
				{
					int num = text.IndexOf('=');
					if (num > 0 && int.TryParse(text.Substring(0, num), out var result) && num + 1 < text.Length)
					{
						list.Add(new KeyValuePair<int, string>(result, text.Substring(num + 1)));
					}
				}
			}
			return list;
		}
	}
	internal static class ClassIntroText
	{
		public static string TitlePrefix
		{
			get
			{
				if (!ConfigLocalization.UseEnglish)
				{
					return "[职业介绍] ";
				}
				return "[Class Intro] ";
			}
		}

		public static string Keywords(int code)
		{
			if (!ConfigLocalization.UseEnglish)
			{
				return KeywordsZh(code);
			}
			return KeywordsEn(code);
		}

		public static string Intro(int code)
		{
			if (!ConfigLocalization.UseEnglish)
			{
				return IntroZh(code);
			}
			return IntroEn(code);
		}

		public static string KeywordsLine(int code)
		{
			string text = Keywords(code);
			if (string.IsNullOrEmpty(text))
			{
				return string.Empty;
			}
			if (!ConfigLocalization.UseEnglish)
			{
				return "关键词:" + text;
			}
			return "Keywords: " + text;
		}

		public static string IntroLine(int code)
		{
			string text = Intro(code);
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
			return string.Empty;
		}

		private static string KeywordsZh(int code)
		{
			return code switch
			{
				1 => "每关随机道具、一次性昏迷复活、高饥饿消耗、无限周期加速", 
				2 => "范围增益、开局负债、绑定全员生死、亡语激励、额外神秘物品", 
				3 => "周期随机补给、开局背包、初始高石化", 
				4 => "每关医疗物资、范围减伤解毒、队友死亡自伤惩罚", 
				5 => "初始武器、匕首吹箭、拾取转化", 
				6 => "范围潘多拉、拾取转化、概率消失、持续自增诅咒", 
				7 => "骸骨之书、死亡增益、骷髅强化、初始高诅咒、诅咒与增益运营", 
				8 => "毒孢抗性、自然食物、周期粉蕉、禁用工具", 
				9 => "负面状态转化、无负面周期惩罚、周期负面耐受", 
				10 => "开局随机物资腰包、独行流血、集体行动、同伴增益", 
				11 => "检查点旗标、插旗范围清负、自身插旗石化、全员随机增益、团队爆发", 
				12 => "跳舞回体、强制共舞、周期后空翻、摔倒风险", 
				13 => "周期产蛋、传染转职、同类大范围回血、周期后空翻", 
				14 => "禁用物品、拾取消失、周期重置、受伤放大、职业免疫", 
				15 => "周期卷轴、双形态、随机效果、负面代价", 
				16 => "初始高负面、自带黄金抓钩、抓人高惩罚、潘多拉免疫", 
				17 => "便携烤炉、烤物解饿、烤多中毒、非食物奖励、持炉炎热", 
				18 => "便携烤炉、生食烹饪、群体增益、熟食增益、生食惩罚、饥饿昏迷", 
				19 => "踢击替代拉人、踢中回体、踢空困倦、初始诅咒", 
				20 => "开局背包、物品囤积、负重奖励、周期加速、额外体力", 
				21 => "昏迷救援、传送无敌、救起加速、背人治疗、死亡惩罚", 
				22 => "防晒喷雾、每关刷新、强制覆盖、防晒回体、用完额外效果", 
				23 => "独行奖励、结伴惩罚、半径检测", 
				24 => "童子军大炮、每关与周期获得、受伤炸药、主手风险", 
				25 => "喇叭乐器、演奏范围增益、拾取转化、库存奖励、初始诅咒、特殊转职", 
				26 => "童军宝石、诅咒锁定、拾取消失、特殊增益、零号守则、天底解放", 
				27 => "早起虫儿、攀爬高频脱手、分解垃圾奖励、感染转职、同类增益、免疫孢子、食虫惩罚", 
				28 => "周期绑气球、拾取转化、只吃包装熟食、冷热抵抗、额外负面", 
				29 => "周期荔莓、食用奖励、非莓惩罚、进食损伤、特殊转职", 
				30 => "周期获得昆虫、额外体力溢出取模、夜间加速、独行清负、职业效果复制", 
				31 => "自带绳索、丢绳损伤、拾绳治疗、沉默奖励、挂绳超时损伤", 
				_ => string.Empty, 
			};
		}

		private static string IntroZh(int code)
		{
			return code switch
			{
				1 => "【开路先锋】每关获得随机道具背包;首次昏迷将完全清除负面复活;【高代谢】迫使你不断管理饥饿与冲刺。", 
				2 => "【团队核心】持续强力范围增益;自身诅咒石化随全员生死波动;自身死亡触发全员激励;全员存活时,自己开启雕像可获额外物品。", 
				3 => "【移动补给】开局自带随机背包;定时获得随机物资;初始诅咒与较高石化需谨慎管理;与【炊事员】有特殊羁绊。", 
				4 => "【随队医护】每关获得随机医疗物资背包;近身小范围持续减伤解毒;队友阵亡会触发自伤惩罚。", 
				5 => "【生死问题】开局自带仪式匕首与吹箭筒;拾取物品概率变为武器;杀人还是救人,这是一个问题。", 
				6 => "【混沌之子】持续向周围散播潘多拉效果;拾取物品半变魔盒半消失;自身诅咒持续叠加。", 
				7 => "【骷髅之王】致敬本模组最不小团体的职业;通过玩家死亡、与骷髅配合降低自身诅咒;持续为周围的人类玩家施加中毒、骷髅玩家获得额外体力与无敌。", 
				8 => "【自然力量】自身对中毒与孢子有较强抗性;只能食用生的自然食物,否则获得损伤;定时获得粉莓蕉;自身无法主动使用攀爬道具,拾取会直接变为木棍。", 
				9 => "【负面转化】受到绝大部分负面状态时,等量转化额外体力;身上无负面状态则周期损失额外体力;对特定负面状态有耐受。", 
				10 => "【结伴而行】开局自带随机物资腰包;落单独行持续流血与饥饿;需和玩家结伴同行缓解,与同职业【童军】与【领袖】有加强羁绊。", 
				11 => "【团队旗手】每关获得检查点旗标;自身插旗立即范围清除负面,三秒后全员获得随机增益效果;自身每次插旗增加石化。", 
				12 => "【一起跳舞】自身范围内玩家跳舞获得额外体力;自身跳舞强制周围玩家共舞;定时强制后空翻且必定摔倒。", 
				13 => "【模因传播】语音与产蛋周期获得额外体力;传染式转职,可主动通过喂食煎蛋强制同化他人;较大范围内,同类周期回血减伤,但定时强制后空翻且必定摔倒。", 
				14 => "【请输入文本】禁用食物及绝大部分道具,拾取物品直接消失;每关饥饿值清零重置;周期回满额外体力;受伤放大即倒;免疫【潘多拉之子】【死灵法师】【舞王】【区】职业效果。", 
				15 => "【随机施法】定时获得卷轴,打开随机触发效果并增加自身石化/诅咒;人类形态效果偏辅助,骷髅形态效果高收益高代价。", 
				16 => "【立体机动】初始高诅咒高石化需谨慎管理;开局自带黄金抓钩与滑翔翼;使用抓钩勾中玩家会立即触发高损伤惩罚;免疫【潘多拉之子】且拾取的潘多拉餐盒直接消失。", 
				17 => "【烤炉大师】定时获得便携烤炉;烤任意物品都能解饿;烤多会中毒;周期烤非食物(道具)可获额外体力;手持便携烤炉时有高频炎热。", 
				18 => "【饮食保障】定时获得便携烤炉;烤熟生食可为周围回饥饿与体力;食用熟食有额外收益,食用生食有较大惩罚;附近有人饥饿会致困昏迷;与【后勤】有特殊羁绊。", 
				19 => "【并非和平】右键拉人变为踢人;踢中玩家获得额外体力,踢空加困倦;开局自带诅咒。", 
				20 => "【负重前行】开局自带背包;负重过半达标后,周期获得加速与额外体力。", 
				21 => "【火线救援】队友昏迷将自动传送并获得一次无敌,背起再获得一次加速与大量额外体力;背起玩家时治疗双方;有玩家死亡会获得石化惩罚。", 
				22 => "【精致防晒】每关获得三瓶防晒喷雾,强制覆盖物品栏;获得防晒奖励额外体力,每次用完防晒喷雾会额外奖励额外体力并积累石化。", 
				23 => "【单人模式】离人够远时周期奖励额外体力;靠近他人持续困倦。", 
				24 => "【重火力者】每关获得童子军大炮;定时获得童子军大炮;受伤就会获得炸药,且物品栏满时会直接塞入主手。", 
				25 => "【演奏乐队】拾取物品概率变乐器,库存与演奏可范围奖励额外体力;库存集齐三种乐器有额外加速奖励,且给予他人有低概率将其转职。", 
				26 => "【领队残魂】初始诅咒锁定,仅能通过获取童军宝石道具降低;使用童军宝石道具有专属特殊增益;禁用童军宝石道具以外物品,拾取物品直接消失;即时传送救援,不可放弃他人;集齐所有童军宝石道具有额外奖励;抵达天底解放灵魂全员奖励。", 
				27 => "【蠕动之区】攀爬时高频强制脱手;靠拾取分解垃圾物品、获取积攒早起虫儿积累收益;感染式转职,周期对附近施加孢子,过半会使他人强制同化,并获得早起虫儿;同类聚集有加速与无限体力效果奖励;自身免疫孢子;食用早期虫儿有诅咒惩罚。", 
				28 => "【太空漂流】身上定时绑上气球;无法拾取气球/一束气球;拾取物品概率变为火箭背包/喷气背包;只吃包装食品或熟食,否则中毒;对冷热有一定抗性,但部分负面状态会额外加重。", 
				29 => "【大果成瘾】定时获得青/紫荔莓;食用青荔莓获得冲刺效果、紫荔莓则给无限体力;食用其他食物加困倦;每次进食固定损伤;他人多次连吃荔莓会被强制同化。", 
				30 => "【代码之力】定时获得随机虫类,获得时大概率中毒;额外体力溢出会取模重置;夜晚周期获得加速;独行会周期清负面;无负面且无虫时会复制在场他人职业效果,并持续生效一段时间。", 
				31 => "【带妻出行】每关自带一条绳索;【心痛】失去绳索(丢弃 / 展开使用)时增加损伤;【收编后宫】拾取绳索时减少损伤;【沉默者】每隔十五秒检测一次,没在讲话则增加额外体力;【自挂东南枝】连续挂在绳索上超过三秒会一次性承受大量损伤。", 
				_ => string.Empty, 
			};
		}

		private static string KeywordsEn(int code)
		{
			return code switch
			{
				1 => "Random items each level · One-time faint revive · High hunger drain · Periodic infinite sprint", 
				2 => "Area buffs · Opening debt · Bound to everyone's life · Death-triggered rally · Extra mystery item", 
				3 => "Periodic random supply · Starting backpack · High starting petrify", 
				4 => "Medical supplies each level · Area injury/poison relief · Self-injury on teammate death", 
				5 => "Starting weapons · Dagger and blowgun · Pickup conversion", 
				6 => "Area Pandora · Pickup conversion · Chance to vanish · Steadily rising curse", 
				7 => "Book of Bones · Death buffs · Skeleton empowerment · High starting curse · Curse/gain management", 
				8 => "Poison/spore resistance · Natural food only · Periodic pink banana · Tools disabled", 
				9 => "Debuff conversion · Penalty without debuffs · Periodic debuff tolerance", 
				10 => "Starting random fanny pack · Solo bleeding · Group action · Companion buffs", 
				11 => "Checkpoint flag · Area debuff purge on plant · Self petrify on plant · Random team buffs · Team burst", 
				12 => "Dance for stamina · Forced group dance · Periodic backflip · Fall risk", 
				13 => "Periodic eggs · Contagious reclass · Same-class area healing · Periodic backflip", 
				14 => "Items disabled · Pickups vanish · Periodic reset · Amplified injury · Class immunity", 
				15 => "Periodic scrolls · Two forms · Random effects · Negative cost", 
				16 => "High starting debuffs · Golden grapnel · Heavy grab penalty · Pandora immunity", 
				17 => "Portable stove · Cooking sates hunger · Cooking builds poison · Non-food rewards · Heat while holding", 
				18 => "Portable stove · Cooking raw food · Group buffs · Cooked-food bonus · Raw-food penalty · Hunger-induced drowsiness", 
				19 => "Kick replaces grab · Hits restore stamina · Misses cause drowsiness · Starting curse", 
				20 => "Starting backpack · Item hoarding · Load reward · Periodic sprint · Extra stamina", 
				21 => "Faint rescue · Teleport invincibility · Post-rescue sprint · Carrying heals both · Death penalty", 
				22 => "Sunscreen · Refreshed each level · Forced overwrite · Sunscreen stamina · Per-bottle bonus", 
				23 => "Solo reward · Company penalty · Radius check", 
				24 => "Scout cannon · Each level and periodic · Injury dynamite · Main-hand risk", 
				25 => "Horns and instruments · Performance area buff · Pickup conversion · Inventory bonus · Starting curse · Special reclass", 
				26 => "Scout gems · Locked curse · Pickups vanish · Special bonuses · Rule Zero · Nadir liberation", 
				27 => "Early bug · Frequent climbing slips · Decomposing junk rewards · Contagious reclass · Same-class buffs · Spore immunity · Bug-eating penalty", 
				28 => "Periodic balloons · Pickup conversion · Packaged/cooked food only · Heat/cold resistance · Extra debuffs", 
				29 => "Periodic kingberries · Eating rewards · Non-berry penalty · Injury per meal · Special reclass", 
				30 => "Periodic insects · Extra stamina overflow modulo · Night sprint · Solo debuff cleanse · Class-effect copy", 
				31 => "Starts with a rope · Losing a rope costs injury · Picking one up relieves injury · Silence reward · Long rope hangs hurt", 
				_ => string.Empty, 
			};
		}

		private static string IntroEn(int code)
		{
			return code switch
			{
				1 => "【Pioneer】 a random-item backpack every level; your first faint fully clears debuffs as you revive; 【Hyper-metabolism】 forces you to keep managing hunger and sprints.", 
				2 => "【Team core】 strong sustained area buffs; your curse and petrify swing with everyone's life and death; your death rallies everyone; while all are alive, opening an Ancient Idol yourself yields an extra item.", 
				3 => "【Mobile supply】 starts with a random backpack; receives random supplies on a timer; your starting curse and high petrify need careful management; special bond with the Cook.", 
				4 => "【Field medic】 a random medical backpack every level; sustained close-range injury and poison relief; a teammate's death triggers a self-injury penalty.", 
				5 => "【A matter of life and death】 starts with a ritual dagger and a blowgun; picked-up items may become weapons; to kill or to save, that is the question.", 
				6 => "【Child of chaos】 keeps spreading the Pandora effect around you; half of the items you pick up become boxes and half vanish; your curse keeps stacking.", 
				7 => "【King of skeletons】 a tribute to the mod's least small group; lower your curse through player deaths and skeleton allies; keep poisoning nearby humans while skeletons gain extra stamina and invincibility.", 
				8 => "【Power of nature】 strong resistance to poison and spores; you can only eat raw natural food or take injury; gains a pink banana on a timer; climbing tools are unusable — picked-up ones turn into wooden sticks.", 
				9 => "【Negative conversion】 most debuffs you take convert into an equal amount of extra stamina; with no debuffs you periodically lose extra stamina; tolerant against specific debuffs.", 
				10 => "【Better together】 starts with a random-item fanny pack; alone you bleed and starve; you need company to ease it; strengthened bond with fellow Scouts and Leaders.", 
				11 => "【Team standard-bearer】 a checkpoint flag every level; planting it immediately clears debuffs in range and 3 s later grants everyone random buffs; each plant adds petrify to yourself.", 
				12 => "【Dance together】 players dancing in your range grant extra stamina; your own dance forces nearby players to dance; periodic forced backflips that always end in a fall.", 
				13 => "【Meme spread】 voice and egg cycles grant extra stamina; contagious reclassing — you can force-merge others by feeding fried eggs; same-class players heal and take less injury over a larger range, but periodic backflips always end in a fall.", 
				14 => "【Input text here】 food and most items are disabled and picked-up items vanish; hunger resets to zero every level; extra stamina refills periodically; any injury is amplified into a knockdown; immune to the Pandora, Necromancer, Dancer and Qu class effects.", 
				15 => "【Random casting】 scrolls on a timer; opening one triggers a random effect and adds petrify/curse; the human form leans support, the skeleton form trades a high cost for high gain.", 
				16 => "【Aerial mobility】 high starting curse and petrify need careful management; starts with a golden grapnel and a glider; hooking a player with the grapnel immediately triggers a heavy injury penalty; immune to Pandora effects and picked-up Pandora lunchboxes vanish.", 
				17 => "【Grill master】 portable stoves on a timer; cooking anything sates hunger; cooking too much poisons you; periodically cooking non-food (tools) grants extra stamina; holding a portable stove causes frequent heat.", 
				18 => "【Food security】 portable stoves on a timer; cooking raw food restores hunger and stamina around you; cooked food pays extra, raw food punishes hard; nearby hungry players make you drowsy; special bond with the Logistician.", 
				19 => "【Not so peaceful】 the right-click grab becomes a kick; kicking a player grants extra stamina, missing adds drowsiness; starts with curse.", 
				20 => "【Carrying the weight】 starts with a backpack; once your load passes the threshold you periodically gain a sprint and extra stamina.", 
				21 => "【Fireline rescue】 a teammate's faint auto-teleports you with invincibility; picking them up grants a sprint and a large amount of extra stamina; carrying a player heals both; a player's death gives you petrify.", 
				22 => "【Exquisite sunscreen】 three sunscreens every level, force-overwriting your inventory; gaining sunscreen grants extra stamina, and finishing each bottle grants more extra stamina plus petrify.", 
				23 => "【Single-player mode】 periodically rewarded with extra stamina when far from others; constant drowsiness when someone is close.", 
				24 => "【Heavy gunner】 a scout cannon every level; scout cannons on a timer; taking damage grants dynamite, which is forced into your main hand when the inventory is full.", 
				25 => "【The playing band】 picked-up items may become instruments; inventory and performance grant area extra stamina; holding all three instruments grants an extra sprint, and giving them to others has a low chance to reclass them.", 
				26 => "【Scoutmaster's soul】 starting curse is locked and only Scout gem items can lower it; Scout gems grant exclusive bonuses; all non-gem items are disabled and picked-up items vanish; instant teleport rescue, never abandon others; collecting every Scout gem grants an extra reward; freeing the soul at the Nadir rewards everyone.", 
				27 => "【Wriggling Qu】 frequent forced let-go while climbing; profit from decomposing junk items and accumulating early bugs; contagious reclassing — periodically applies spores nearby, and past the threshold forces others to merge and grants early bugs; same-class clusters get sprint and infinite stamina rewards; immune to spores; eating early bugs costs curse.", 
				28 => "【Space drift】 balloons are tied on periodically; balloons and bunches of balloons cannot be picked up; picked-up items may become rocket packs or jetpacks; only packaged or cooked food, otherwise poison; some heat/cold resistance but certain debuffs hit harder.", 
				29 => "【Big-berry addiction】 green/purple kingberries on a timer; green grants a sprint, purple grants infinite stamina; other food adds drowsiness; every meal costs injury; others who chain-eat kingberries are forced to merge.", 
				30 => "【Power of code】 random insects on a timer, usually with poison; extra stamina overflow wraps around; periodic sprint at night; solo play periodically cleanses debuffs; with no debuffs and no insects you copy another present class's effect for a while.", 
				31 => "【Bringing the wife along】 one rope every level; 【Heartache】 losing a rope (dropping or deploying it) adds injury; 【Taking in concubines】 picking a rope up relieves injury; 【The silent one】 every 15 s, if you are not speaking you gain extra stamina; 【Hanging from the southeast branch】 staying on a rope for more than 3 s costs a heavy one-time injury.", 
				_ => string.Empty, 
			};
		}
	}
	public sealed class ClassLedger
	{
		private readonly Dictionary<string, ClassType> _byId = new Dictionary<string, ClassType>();

		private readonly Dictionary<PhotonView, string> _idByView = new Dictionary<PhotonView, string>();

		private readonly Dictionary<string, int> _claimById = new Dictionary<string, int>();

		private readonly Dictionary<PhotonView, ClassType> _classByView = new Dictionary<PhotonView, ClassType>();

		private readonly Dictionary<ClassType, int> _countByType = new Dictionary<ClassType, int>();

		public int Count => _byId.Count;

		public void Clear()
		{
			_byId.Clear();
			_idByView.Clear();
			_classByView.Clear();
			_claimById.Clear();
			_countByType.Clear();
		}

		private void SetEntry(string id, ClassType type)
		{
			if (!_byId.TryGetValue(id, out var value) || value != type)
			{
				if (_byId.TryGetValue(id, out var value2) && value2 != ClassType.None)
				{
					_countByType[value2] = Mathf.Max(0, (_countByType.TryGetValue(value2, out var value3) ? value3 : 0) - 1);
				}
				_byId[id] = type;
				if (type != ClassType.None)
				{
					_countByType[type] = (_countByType.TryGetValue(type, out var value4) ? value4 : 0) + 1;
				}
				_classByView.Clear();
			}
		}

		public bool HasClassInRoom(ClassType type)
		{
			if (_countByType.TryGetValue(type, out var value))
			{
				return value > 0;
			}
			return false;
		}

		public bool TryGet(string id, out ClassType type)
		{
			type = ClassType.None;
			if (!string.IsNullOrEmpty(id))
			{
				return _byId.TryGetValue(id, out type);
			}
			return false;
		}

		public string IdOf(Character character)
		{
			if ((Object)(object)character == (Object)null)
			{
				return null;
			}
			PhotonView photonView = ((MonoBehaviourPun)character).photonView;
			if ((Object)(object)photonView == (Object)null)
			{
				return null;
			}
			if (_idByView.TryGetValue(photonView, out var value))
			{
				return value;
			}
			int num = ((photonView.Owner != null) ? photonView.Owner.ActorNumber : (-photonView.ViewID));
			List<string> list = new List<string>(4);
			HostApi.FillStableIdCandidates(character, list);
			if (list.Count == 0)
			{
				return null;
			}
			string text = null;
			for (int i = 0; i < list.Count; i++)
			{
				string text2 = list[i];
				if (!_claimById.TryGetValue(text2, out var value2) || value2 == num || !IsClaimActive(value2))
				{
					text = text2;
					break;
				}
			}
			if (string.IsNullOrEmpty(text))
			{
				text = ((photonView.Owner != null) ? ("a:" + photonView.Owner.ActorNumber) : ("v:" + photonView.ViewID));
			}
			_idByView[photonView] = text;
			_claimById[text] = num;
			if (list.Count > 1)
			{
				Plugin.Log.LogInfo((object)("[ClassLedger] 稳定标识:" + Nick(character) + " → " + text + "(候选:" + string.Join(" | ", list) + ")"));
			}
			else
			{
				Plugin.Log.LogInfo((object)("[ClassLedger] 稳定标识:" + Nick(character) + " → " + text));
			}
			return text;
		}

		private static bool IsClaimActive(int claimKey)
		{
			if (claimKey == 0)
			{
				return true;
			}
			if (claimKey > 0)
			{
				Room currentRoom = PhotonNetwork.CurrentRoom;
				if (currentRoom == null)
				{
					return true;
				}
				return currentRoom.GetPlayer(claimKey, false) != null;
			}
			return (Object)(object)PhotonView.Find(-claimKey) != (Object)null;
		}

		public ClassType GetClass(Character character)
		{
			if ((Object)(object)character == (Object)null)
			{
				return ClassType.None;
			}
			PhotonView photonView = ((MonoBehaviourPun)character).photonView;
			if ((Object)(object)photonView == (Object)null)
			{
				return ClassType.None;
			}
			if (_classByView.TryGetValue(photonView, out var value))
			{
				return value;
			}
			ClassType type;
			ClassType classType = (TryGet(IdOf(character), out type) ? type : ClassType.None);
			_classByView[photonView] = classType;
			return classType;
		}

		public int TotalCapacity()
		{
			int num = 0;
			if (ModConfig.Explorer.Enabled.Value)
			{
				int num2 = MaxOf(ClassType.Explorer);
				if (num2 <= 0)
				{
					return int.MaxValue;
				}
				num += num2;
			}
			if (ModConfig.Leader.Enabled.Value)
			{
				int num3 = MaxOf(ClassType.Leader);
				if (num3 <= 0)
				{
					return int.MaxValue;
				}
				num += num3;
			}
			if (ModConfig.Logistician.Enabled.Value)
			{
				int num4 = MaxOf(ClassType.Logistician);
				if (num4 <= 0)
				{
					return int.MaxValue;
				}
				num += num4;
			}
			if (ModConfig.Doctor.Enabled.Value)
			{
				int num5 = MaxOf(ClassType.Doctor);
				if (num5 <= 0)
				{
					return int.MaxValue;
				}
				num += num5;
			}
			if (ModConfig.Assassin.Enabled.Value)
			{
				int num6 = MaxOf(ClassType.Assassin);
				if (num6 <= 0)
				{
					return int.MaxValue;
				}
				num += num6;
			}
			if (ModConfig.Pandora.Enabled.Value)
			{
				int num7 = MaxOf(ClassType.Pandora);
				if (num7 <= 0)
				{
					return int.MaxValue;
				}
				num += num7;
			}
			if (ModConfig.Necromancer.Enabled.Value)
			{
				int num8 = MaxOf(ClassType.Necromancer);
				if (num8 <= 0)
				{
					return int.MaxValue;
				}
				num += num8;
			}
			if (ModConfig.M.Enabled.Value)
			{
				int num9 = MaxOf(ClassType.M);
				if (num9 <= 0)
				{
					return int.MaxValue;
				}
				num += num9;
			}
			if (ModConfig.Savage.Enabled.Value)
			{
				int num10 = MaxOf(ClassType.Savage);
				if (num10 <= 0)
				{
					return int.MaxValue;
				}
				num += num10;
			}
			if (ModConfig.Scout.Enabled.Value)
			{
				int num11 = MaxOf(ClassType.Scout);
				if (num11 <= 0)
				{
					return int.MaxValue;
				}
				num += num11;
			}
			if (ModConfig.StandardBearer.Enabled.Value)
			{
				int num12 = MaxOf(ClassType.StandardBearer);
				if (num12 <= 0)
				{
					return int.MaxValue;
				}
				num += num12;
			}
			if (ModConfig.Dancer.Enabled.Value)
			{
				int num13 = MaxOf(ClassType.Dancer);
				if (num13 <= 0)
				{
					return int.MaxValue;
				}
				num += num13;
			}
			if (ModConfig.Daihao.Enabled.Value)
			{
				int num14 = MaxOf(ClassType.Daihao);
				if (num14 <= 0)
				{
					return int.MaxValue;
				}
				num += num14;
			}
			if (ModConfig.Saint.Enabled.Value)
			{
				int num15 = MaxOf(ClassType.Saint);
				if (num15 <= 0)
				{
					return int.MaxValue;
				}
				num += num15;
			}
			if (ModConfig.Mage.Enabled.Value)
			{
				int num16 = MaxOf(ClassType.Mage);
				if (num16 <= 0)
				{
					return int.MaxValue;
				}
				num += num16;
			}
			if (ModConfig.Challenger.Enabled.Value)
			{
				int num17 = MaxOf(ClassType.Challenger);
				if (num17 <= 0)
				{
					return int.MaxValue;
				}
				num += num17;
			}
			if (ModConfig.MasterChef.Enabled.Value)
			{
				int num18 = MaxOf(ClassType.MasterChef);
				if (num18 <= 0)
				{
					return int.MaxValue;
				}
				num += num18;
			}
			if (ModConfig.Cook.Enabled.Value)
			{
				int num19 = MaxOf(ClassType.Cook);
				if (num19 <= 0)
				{
					return int.MaxValue;
				}
				num += num19;
			}
			if (ModConfig.Pacifist.Enabled.Value)
			{
				int num20 = MaxOf(ClassType.Pacifist);
				if (num20 <= 0)
				{
					return int.MaxValue;
				}
				num += num20;
			}
			if (ModConfig.Hamster.Enabled.Value)
			{
				int num21 = MaxOf(ClassType.Hamster);
				if (num21 <= 0)
				{
					return int.MaxValue;
				}
				num += num21;
			}
			if (ModConfig.Firefighter.Enabled.Value)
			{
				int num22 = MaxOf(ClassType.Firefighter);
				if (num22 <= 0)
				{
					return int.MaxValue;
				}
				num += num22;
			}
			if (ModConfig.Narcissist.Enabled.Value)
			{
				int num23 = MaxOf(ClassType.Narcissist);
				if (num23 <= 0)
				{
					return int.MaxValue;
				}
				num += num23;
			}
			if (ModConfig.LoneWolf.Enabled.Value)
			{
				int num24 = MaxOf(ClassType.LoneWolf);
				if (num24 <= 0)
				{
					return int.MaxValue;
				}
				num += num24;
			}
			if (ModConfig.Gunner.Enabled.Value)
			{
				int num25 = MaxOf(ClassType.Gunner);
				if (num25 <= 0)
				{
					return int.MaxValue;
				}
				num += num25;
			}
			if (ModConfig.Band.Enabled.Value)
			{
				int num26 = MaxOf(ClassType.Band);
				if (num26 <= 0)
				{
					return int.MaxValue;
				}
				num += num26;
			}
			if (ModConfig.MilesSoul.Enabled.Value)
			{
				int num27 = MaxOf(ClassType.MilesSoul);
				if (num27 <= 0)
				{
					return int.MaxValue;
				}
				num += num27;
			}
			if (ModConfig.Qu.Enabled.Value)
			{
				int num28 = MaxOf(ClassType.Qu);
				if (num28 <= 0)
				{
					return int.MaxValue;
				}
				num += num28;
			}
			if (ModConfig.Astronaut.Enabled.Value)
			{
				int num29 = MaxOf(ClassType.Astronaut);
				if (num29 <= 0)
				{
					return int.MaxValue;
				}
				num += num29;
			}
			if (ModConfig.Langwang.Enabled.Value)
			{
				int num30 = MaxOf(ClassType.Langwang);
				if (num30 <= 0)
				{
					return int.MaxValue;
				}
				num += num30;
			}
			if (ModConfig.Programmer.Enabled.Value)
			{
				int num31 = MaxOf(ClassType.Programmer);
				if (num31 <= 0)
				{
					return int.MaxValue;
				}
				num += num31;
			}
			if (ModConfig.ATacv.Enabled.Value)
			{
				int num32 = MaxOf(ClassType.ATacv);
				if (num32 <= 0)
				{
					return int.MaxValue;
				}
				num += num32;
			}
			return num;
		}

		public bool IsFull()
		{
			return Count >= TotalCapacity();
		}

		public ClassType Assign(Character character)
		{
			string text = IdOf(character);
			if (string.IsNullOrEmpty(text))
			{
				return ClassType.None;
			}
			List<ClassType> enabledClasses = GetEnabledClasses();
			if (enabledClasses.Count == 0)
			{
				return ClassType.None;
			}
			ClassType classType = (ModConfig.General.RandomAssign.Value ? PickByWeight(enabledClasses) : PickInOrder(enabledClasses));
			if (classType == ClassType.None)
			{
				Plugin.Log.LogInfo((object)("[ClassLedger] 所有职业名额已满," + Nick(character) + " 本次不分配职业。"));
				return ClassType.None;
			}
			SetEntry(text, classType);
			Plugin.Log.LogInfo((object)("[ClassLedger] 分配职业:" + Nick(character) + " → " + Describe(classType)));
			return classType;
		}

		public bool TryReassign(Character character, out ClassType oldType, out ClassType newType)
		{
			oldType = ClassType.None;
			newType = ClassType.None;
			string text = IdOf(character);
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			if (!_byId.TryGetValue(text, out oldType))
			{
				return false;
			}
			List<ClassType> list = new List<ClassType>();
			foreach (ClassType enabledClass in GetEnabledClasses())
			{
				if (enabledClass != oldType && !IsCapped(enabledClass, CountOf(enabledClass)))
				{
					list.Add(enabledClass);
				}
			}
			if (list.Count == 0)
			{
				return false;
			}
			newType = WeightedPick(list);
			if (newType == ClassType.None)
			{
				return false;
			}
			SetEntry(text, newType);
			return true;
		}

		public bool ForceAssign(Character character, ClassType type)
		{
			if (type == ClassType.None)
			{
				return false;
			}
			string text = IdOf(character);
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			if (_byId.TryGetValue(text, out var value) && value == type)
			{
				return false;
			}
			SetEntry(text, type);
			return true;
		}

		private static string Nick(Character character)
		{
			if ((Object)(object)character == (Object)null || (Object)(object)((MonoBehaviourPun)character).photonView == (Object)null || ((MonoBehaviourPun)character).photonView.Owner == null)
			{
				return "?";
			}
			return $"{((MonoBehaviourPun)character).photonView.Owner.NickName}(#{((MonoBehaviourPun)character).photonView.Owner.ActorNumber})";
		}

		private static List<ClassType> GetEnabledClasses()
		{
			List<ClassType> list = new List<ClassType>();
			if (ModConfig.Explorer.Enabled.Value)
			{
				list.Add(ClassType.Explorer);
			}
			if (ModConfig.Leader.Enabled.Value)
			{
				list.Add(ClassType.Leader);
			}
			if (ModConfig.Logistician.Enabled.Value)
			{
				list.Add(ClassType.Logistician);
			}
			if (ModConfig.Doctor.Enabled.Value)
			{
				list.Add(ClassType.Doctor);
			}
			if (ModConfig.Assassin.Enabled.Value)
			{
				list.Add(ClassType.Assassin);
			}
			if (ModConfig.Pandora.Enabled.Value)
			{
				list.Add(ClassType.Pandora);
			}
			if (ModConfig.Necromancer.Enabled.Value)
			{
				list.Add(ClassType.Necromancer);
			}
			if (ModConfig.M.Enabled.Value)
			{
				list.Add(ClassType.M);
			}
			if (ModConfig.Savage.Enabled.Value)
			{
				list.Add(ClassType.Savage);
			}
			if (ModConfig.Scout.Enabled.Value)
			{
				list.Add(ClassType.Scout);
			}
			if (ModConfig.StandardBearer.Enabled.Value)
			{
				list.Add(ClassType.StandardBearer);
			}
			if (ModConfig.Dancer.Enabled.Value)
			{
				list.Add(ClassType.Dancer);
			}
			if (ModConfig.Daihao.Enabled.Value)
			{
				list.Add(ClassType.Daihao);
			}
			if (ModConfig.Saint.Enabled.Value)
			{
				list.Add(ClassType.Saint);
			}
			if (ModConfig.Mage.Enabled.Value)
			{
				list.Add(ClassType.Mage);
			}
			if (ModConfig.Challenger.Enabled.Value)
			{
				list.Add(ClassType.Challenger);
			}
			if (ModConfig.MasterChef.Enabled.Value)
			{
				list.Add(ClassType.MasterChef);
			}
			if (ModConfig.Cook.Enabled.Value)
			{
				list.Add(ClassType.Cook);
			}
			if (ModConfig.Pacifist.Enabled.Value)
			{
				list.Add(ClassType.Pacifist);
			}
			if (ModConfig.Hamster.Enabled.Value)
			{
				list.Add(ClassType.Hamster);
			}
			if (ModConfig.Firefighter.Enabled.Value)
			{
				list.Add(ClassType.Firefighter);
			}
			if (ModConfig.Narcissist.Enabled.Value)
			{
				list.Add(ClassType.Narcissist);
			}
			if (ModConfig.LoneWolf.Enabled.Value)
			{
				list.Add(ClassType.LoneWolf);
			}
			if (ModConfig.Gunner.Enabled.Value)
			{
				list.Add(ClassType.Gunner);
			}
			if (ModConfig.Band.Enabled.Value)
			{
				list.Add(ClassType.Band);
			}
			if (ModConfig.MilesSoul.Enabled.Value && MilesSoulSelectableNow())
			{
				list.Add(ClassType.MilesSoul);
			}
			if (ModConfig.Qu.Enabled.Value)
			{
				list.Add(ClassType.Qu);
			}
			if (ModConfig.Astronaut.Enabled.Value)
			{
				list.Add(ClassType.Astronaut);
			}
			if (ModConfig.Langwang.Enabled.Value)
			{
				list.Add(ClassType.Langwang);
			}
			if (ModConfig.Programmer.Enabled.Value)
			{
				list.Add(ClassType.Programmer);
			}
			if (ModConfig.ATacv.Enabled.Value)
			{
				list.Add(ClassType.ATacv);
			}
			return list;
		}

		private static bool MilesSoulSelectableNow()
		{
			if (ModConfig.MilesSoul.BeachOnly.Value)
			{
				return !HostClassManager.MilesPastBeach();
			}
			return true;
		}

		private int CountOf(ClassType type)
		{
			if (!_countByType.TryGetValue(type, out var value))
			{
				return 0;
			}
			return value;
		}

		private static bool IsCapped(ClassType type, int current)
		{
			int num = MaxOf(type);
			if (num > 0)
			{
				return current >= num;
			}
			return false;
		}

		private ClassType PickByWeight(List<ClassType> enabled)
		{
			List<ClassType> list = new List<ClassType>();
			foreach (ClassType item in enabled)
			{
				int num = CountOf(item);
				if (num < MinOf(item) && !IsCapped(item, num))
				{
					list.Add(item);
				}
			}
			List<ClassType> obj = ((list.Count > 0) ? list : enabled);
			List<ClassType> list2 = new List<ClassType>();
			foreach (ClassType item2 in obj)
			{
				if (!IsCapped(item2, CountOf(item2)))
				{
					list2.Add(item2);
				}
			}
			if (list2.Count == 0)
			{
				return ClassType.None;
			}
			return WeightedPick(list2);
		}

		private ClassType PickInOrder(List<ClassType> enabled)
		{
			foreach (ClassType item in enabled)
			{
				if (!IsCapped(item, CountOf(item)))
				{
					return item;
				}
			}
			return ClassType.None;
		}

		private ClassType WeightedPick(List<ClassType> pool)
		{
			int num = 0;
			foreach (ClassType item in pool)
			{
				num += WeightOf(item);
			}
			if (num <= 0)
			{
				return pool[Random.Range(0, pool.Count)];
			}
			int num2 = Random.Range(0, num);
			int num3 = 0;
			foreach (ClassType item2 in pool)
			{
				num3 += WeightOf(item2);
				if (num2 < num3)
				{
					return item2;
				}
			}
			return pool[pool.Count - 1];
		}

		private static int WeightOf(ClassType type)
		{
			return type switch
			{
				ClassType.Explorer => Mathf.Max(0, ModConfig.Explorer.Weight.Value), 
				ClassType.Leader => Mathf.Max(0, ModConfig.Leader.Weight.Value), 
				ClassType.Logistician => Mathf.Max(0, ModConfig.Logistician.Weight.Value), 
				ClassType.Doctor => Mathf.Max(0, ModConfig.Doctor.Weight.Value), 
				ClassType.Assassin => Mathf.Max(0, ModConfig.Assassin.Weight.Value), 
				ClassType.Pandora => Mathf.Max(0, ModConfig.Pandora.Weight.Value), 
				ClassType.Necromancer => Mathf.Max(0, ModConfig.Necromancer.Weight.Value), 
				ClassType.M => Mathf.Max(0, ModConfig.M.Weight.Value), 
				ClassType.Savage => Mathf.Max(0, ModConfig.Savage.Weight.Value), 
				ClassType.Scout => Mathf.Max(0, ModConfig.Scout.Weight.Value), 
				ClassType.StandardBearer => Mathf.Max(0, ModConfig.StandardBearer.Weight.Value), 
				ClassType.Dancer => Mathf.Max(0, ModConfig.Dancer.Weight.Value), 
				ClassType.Daihao => Mathf.Max(0, ModConfig.Daihao.Weight.Value), 
				ClassType.Saint => Mathf.Max(0, ModConfig.Saint.Weight.Value), 
				ClassType.Mage => Mathf.Max(0, ModConfig.Mage.Weight.Value), 
				ClassType.Challenger => Mathf.Max(0, ModConfig.Challenger.Weight.Value), 
				ClassType.MasterChef => Mathf.Max(0, ModConfig.MasterChef.Weight.Value), 
				ClassType.Cook => Mathf.Max(0, ModConfig.Cook.Weight.Value), 
				ClassType.Pacifist => Mathf.Max(0, ModConfig.Pacifist.Weight.Value), 
				ClassType.Hamster => Mathf.Max(0, ModConfig.Hamster.Weight.Value), 
				ClassType.Firefighter => Mathf.Max(0, ModConfig.Firefighter.Weight.Value), 
				ClassType.Narcissist => Mathf.Max(0, ModConfig.Narcissist.Weight.Value), 
				ClassType.LoneWolf => Mathf.Max(0, ModConfig.LoneWolf.Weight.Value), 
				ClassType.Gunner => Mathf.Max(0, ModConfig.Gunner.Weight.Value), 
				ClassType.Band => Mathf.Max(0, ModConfig.Band.Weight.Value), 
				ClassType.MilesSoul => Mathf.Max(0, ModConfig.MilesSoul.Weight.Value), 
				ClassType.Qu => Mathf.Max(0, ModConfig.Qu.Weight.Value), 
				ClassType.Astronaut => Mathf.Max(0, ModConfig.Astronaut.Weight.Value), 
				ClassType.Langwang => Mathf.Max(0, ModConfig.Langwang.Weight.Value), 
				ClassType.Programmer => Mathf.Max(0, ModConfig.Programmer.Weight.Value), 
				ClassType.ATacv => Mathf.Max(0, ModConfig.ATacv.Weight.Value), 
				_ => 0, 
			};
		}

		private static int MaxOf(ClassType type)
		{
			return type switch
			{
				ClassType.Explorer => Mathf.Max(0, ModConfig.Explorer.MaxPlayers.Value), 
				ClassType.Leader => Mathf.Max(0, ModConfig.Leader.MaxPlayers.Value), 
				ClassType.Logistician => Mathf.Max(0, ModConfig.Logistician.MaxPlayers.Value), 
				ClassType.Doctor => Mathf.Max(0, ModConfig.Doctor.MaxPlayers.Value), 
				ClassType.Assassin => Mathf.Max(0, ModConfig.Assassin.MaxPlayers.Value), 
				ClassType.Pandora => Mathf.Max(0, ModConfig.Pandora.MaxPlayers.Value), 
				ClassType.Necromancer => Mathf.Max(0, ModConfig.Necromancer.MaxPlayers.Value), 
				ClassType.M => Mathf.Max(0, ModConfig.M.MaxPlayers.Value), 
				ClassType.Savage => Mathf.Max(0, ModConfig.Savage.MaxPlayers.Value), 
				ClassType.Scout => Mathf.Max(0, ModConfig.Scout.MaxPlayers.Value), 
				ClassType.StandardBearer => Mathf.Max(0, ModConfig.StandardBearer.MaxPlayers.Value), 
				ClassType.Dancer => Mathf.Max(0, ModConfig.Dancer.MaxPlayers.Value), 
				ClassType.Daihao => Mathf.Max(0, ModConfig.Daihao.MaxPlayers.Value), 
				ClassType.Saint => Mathf.Max(0, ModConfig.Saint.MaxPlayers.Value), 
				ClassType.Mage => Mathf.Max(0, ModConfig.Mage.MaxPlayers.Value), 
				ClassType.Challenger => Mathf.Max(0, ModConfig.Challenger.MaxPlayers.Value), 
				ClassType.MasterChef => Mathf.Max(0, ModConfig.MasterChef.MaxPlayers.Value), 
				ClassType.Cook => Mathf.Max(0, ModConfig.Cook.MaxPlayers.Value), 
				ClassType.Pacifist => Mathf.Max(0, ModConfig.Pacifist.MaxPlayers.Value), 
				ClassType.Hamster => Mathf.Max(0, ModConfig.Hamster.MaxPlayers.Value), 
				ClassType.Firefighter => Mathf.Max(0, ModConfig.Firefighter.MaxPlayers.Value), 
				ClassType.Narcissist => Mathf.Max(0, ModConfig.Narcissist.MaxPlayers.Value), 
				ClassType.LoneWolf => Mathf.Max(0, ModConfig.LoneWolf.MaxPlayers.Value), 
				ClassType.Gunner => Mathf.Max(0, ModConfig.Gunner.MaxPlayers.Value), 
				ClassType.Band => Mathf.Max(0, ModConfig.Band.MaxPlayers.Value), 
				ClassType.MilesSoul => Mathf.Max(0, ModConfig.MilesSoul.MaxPlayers.Value), 
				ClassType.Qu => Mathf.Max(0, ModConfig.Qu.MaxPlayers.Value), 
				ClassType.Astronaut => Mathf.Max(0, ModConfig.Astronaut.MaxPlayers.Value), 
				ClassType.Langwang => Mathf.Max(0, ModConfig.Langwang.MaxPlayers.Value), 
				ClassType.Programmer => Mathf.Max(0, ModConfig.Programmer.MaxPlayers.Value), 
				ClassType.ATacv => Mathf.Max(0, ModConfig.ATacv.MaxPlayers.Value), 
				_ => 0, 
			};
		}

		private static int MinOf(ClassType type)
		{
			return type switch
			{
				ClassType.Explorer => Mathf.Max(0, ModConfig.Explorer.MinPlayers.Value), 
				ClassType.Leader => Mathf.Max(0, ModConfig.Leader.MinPlayers.Value), 
				ClassType.Logistician => Mathf.Max(0, ModConfig.Logistician.MinPlayers.Value), 
				ClassType.Doctor => Mathf.Max(0, ModConfig.Doctor.MinPlayers.Value), 
				ClassType.Assassin => Mathf.Max(0, ModConfig.Assassin.MinPlayers.Value), 
				ClassType.Pandora => Mathf.Max(0, ModConfig.Pandora.MinPlayers.Value), 
				ClassType.Necromancer => Mathf.Max(0, ModConfig.Necromancer.MinPlayers.Value), 
				ClassType.M => Mathf.Max(0, ModConfig.M.MinPlayers.Value), 
				ClassType.Savage => Mathf.Max(0, ModConfig.Savage.MinPlayers.Value), 
				ClassType.Scout => Mathf.Max(0, ModConfig.Scout.MinPlayers.Value), 
				ClassType.StandardBearer => Mathf.Max(0, ModConfig.StandardBearer.MinPlayers.Value), 
				ClassType.Dancer => Mathf.Max(0, ModConfig.Dancer.MinPlayers.Value), 
				ClassType.Daihao => Mathf.Max(0, ModConfig.Daihao.MinPlayers.Value), 
				ClassType.Saint => Mathf.Max(0, ModConfig.Saint.MinPlayers.Value), 
				ClassType.Mage => Mathf.Max(0, ModConfig.Mage.MinPlayers.Value), 
				ClassType.Challenger => Mathf.Max(0, ModConfig.Challenger.MinPlayers.Value), 
				ClassType.MasterChef => Mathf.Max(0, ModConfig.MasterChef.MinPlayers.Value), 
				ClassType.Cook => Mathf.Max(0, ModConfig.Cook.MinPlayers.Value), 
				ClassType.Pacifist => Mathf.Max(0, ModConfig.Pacifist.MinPlayers.Value), 
				ClassType.Hamster => Mathf.Max(0, ModConfig.Hamster.MinPlayers.Value), 
				ClassType.Firefighter => Mathf.Max(0, ModConfig.Firefighter.MinPlayers.Value), 
				ClassType.Narcissist => Mathf.Max(0, ModConfig.Narcissist.MinPlayers.Value), 
				ClassType.LoneWolf => Mathf.Max(0, ModConfig.LoneWolf.MinPlayers.Value), 
				ClassType.Gunner => Mathf.Max(0, ModConfig.Gunner.MinPlayers.Value), 
				ClassType.Band => Mathf.Max(0, ModConfig.Band.MinPlayers.Value), 
				ClassType.MilesSoul => Mathf.Max(0, ModConfig.MilesSoul.MinPlayers.Value), 
				ClassType.Qu => Mathf.Max(0, ModConfig.Qu.MinPlayers.Value), 
				ClassType.Astronaut => Mathf.Max(0, ModConfig.Astronaut.MinPlayers.Value), 
				ClassType.Langwang => Mathf.Max(0, ModConfig.Langwang.MinPlayers.Value), 
				ClassType.Programmer => Mathf.Max(0, ModConfig.Programmer.MinPlayers.Value), 
				ClassType.ATacv => Mathf.Max(0, ModConfig.ATacv.MinPlayers.Value), 
				_ => 0, 
			};
		}

		public static string Describe(ClassType type)
		{
			return type switch
			{
				ClassType.Explorer => "先驱", 
				ClassType.Leader => "领袖", 
				ClassType.Logistician => "后勤", 
				ClassType.Doctor => "医生", 
				ClassType.Assassin => "刺客", 
				ClassType.Pandora => "潘多拉之子", 
				ClassType.Necromancer => "死灵法师", 
				ClassType.M => "M", 
				ClassType.Savage => "野人", 
				ClassType.Scout => "童军", 
				ClassType.StandardBearer => "旗手", 
				ClassType.Dancer => "舞王", 
				ClassType.Daihao => "待皓", 
				ClassType.Saint => "圣人", 
				ClassType.Mage => "法师", 
				ClassType.Challenger => "挑战者", 
				ClassType.MasterChef => "特级厨师", 
				ClassType.Cook => "炊事员", 
				ClassType.Pacifist => "和平主义者", 
				ClassType.Hamster => "仓鼠", 
				ClassType.Firefighter => "救火队员", 
				ClassType.Narcissist => "自恋狂", 
				ClassType.LoneWolf => "独狼", 
				ClassType.Gunner => "炮手", 
				ClassType.Band => "乐队成员", 
				ClassType.MilesSoul => "迈尔斯残魂", 
				ClassType.Qu => "区", 
				ClassType.Astronaut => "太空人", 
				ClassType.Langwang => "榔王", 
				ClassType.Programmer => "程序员", 
				ClassType.ATacv => "ATacv", 
				_ => "无职业", 
			};
		}
	}
	public enum ClassType
	{
		None,
		Explorer,
		Leader,
		Logistician,
		Doctor,
		Assassin,
		Pandora,
		Necromancer,
		M,
		Savage,
		Scout,
		StandardBearer,
		Dancer,
		Daihao,
		Saint,
		Mage,
		Challenger,
		MasterChef,
		Cook,
		Pacifist,
		Hamster,
		Firefighter,
		Narcissist,
		LoneWolf,
		Gunner,
		Band,
		MilesSoul,
		Qu,
		Astronaut,
		Langwang,
		Programmer,
		ATacv
	}
	public static class ModConfig
	{
		public class GeneralConfig
		{
			public ConfigEntry<bool> Enabled { get; private set; }

			public ConfigEntry<bool> RandomAssign { get; private set; }

			public ConfigEntry<float> PollInterval { get; private set; }

			public ConfigEntry<string> ClassListKey { get; private set; }

			public ConfigEntry<string> ClassListMode { get; private set; }

			public ConfigEntry<bool> MageEffectNotify { get; private set; }

			public ConfigEntry<bool> DisplaySync { get; private set; }

			public ConfigEntry<bool> NamePrefix { get; private set; }

			public ConfigEntry<bool> ReclassEnabled { get; private set; }

			public ConfigEntry<string> ReclassItem { get; private set; }

			public ConfigEntry<int> ReclassCount { get; private set; }

			public ConfigEntry<bool> ReclassEmoteEnabled { get; private set; }

			public ConfigEntry<string> ReclassEmoteName { get; private set; }

			public ConfigEntry<bool> NightHeatEnabled { get; private set; }

			public ConfigEntry<float> NightHeatIntervalSeconds { get; private set; }

			public ConfigEntry<float> NightHeatAmount { get; private set; }

			public void Initialize(ConfigFile config)
			{
				Enabled = BindL(config, "通用", "启用模组", defaultValue: true, "总开关。关闭后主机不再分配职业、不再施加任何职业效果。");
				RandomAssign = BindL(config, "通用", "随机分配职业", defaultValue: true, "是否由主机随机分配职业。true=按权重随机分配(无职业选择界面);false=按职业顺序依次分配。");
				PollInterval = BindL(config, "通用", "状态轮询间隔", 0.5f, "主机轮询玩家同步状态的间隔(秒),用于「不屈 / 高代谢 / 共情」的闭环检测。建议 0.25~1。");
				ClassListKey = BindL(config, "通用", "职业显示-按键", "Tab", "按哪个键在主机屏幕上显示「当前职业分配情况」。\n填写 Unity 的 KeyCode 名,例如 Tab、F1、LeftAlt、BackQuote、Mouse2。\n注意:客机没有安装本模组,该面板只能显示在主机(房主)自己的屏幕上。");
				ClassListMode = BindL(config, "通用", "职业显示-模式", ConfigLocalization.UseEnglish ? "HoldToShow" : "按住显示", "职业分配面板的显示方式:\n按住显示 = 按住该键时显示,松开立即隐藏;\n按下显示 = 按一下显示 5 秒后自动隐藏;\n按下切换 = 每按一次在「显示 / 隐藏」之间切换。");
				MageEffectNotify = BindL(config, "通用", "法师效果提示", defaultValue: true, "是否在主机屏幕左上角提示区,显示法师每次触发的是什么效果。\n默认 true。提示形如「法师:XXX 触发【强化】」,显示在左上角、约 8 秒后消失。\n提示只出现在主机(房主)自己的屏幕上;客机未安装本模组,看不到。\n若客机安装了附属模组 com.peakmodding.RollOfTheScoutClient,见下方「职业显示-网络同步」。");
				DisplaySync = BindL(config, "通用", "职业显示-网络同步", defaultValue: true, "是否把「职业分配情况」与「职业效果提示」广播给安装了附属模组\ncom.peakmodding.RollOfTheScoutClient 的客机(供其在客机自己屏幕上显示)。\n原理:主机用 Photon 自定义事件(码 197)周期广播职业快照、并在每次提示时广播该提示文本。\n未安装附属模组的客机完全不受影响(收到即忽略),因此客机仍然零安装可用。\n默认 true。设为 false 可彻底关闭该广播(客机侧将看不到任何职业显示)。");
				NamePrefix = BindL(config, "通用", "头顶职业前缀", defaultValue: true, "是否把「头顶玩家名称」改写成「【职业】昵称」,例如「Rhys」→「【先驱】Rhys」。\n「【职业】」用该玩家进入游戏时的皮肤色着色;「昵称」维持原版颜色不变。\n⚠ 头顶名称是**每个客户端本地**的 UI,因此本开关只影响**本机屏幕**上看到的名字:\n   · 主机:显示本局账本里的职业;\n   · 装了本模组或附属模组的客机:显示主机广播过来的职业(见「职业显示-网络同步」);\n   · 未装任何模组的客机:看不到任何前缀(仍显示原版昵称)。\n默认 true。");
				ReclassEnabled = BindL(config, "通用", "转职-启用", defaultValue: true, "转职机制总开关。true=玩家食用下方「转职-触发物品」后可更换职业;false=完全关闭。\n转职会在「当前未占满名额的其他职业」中按权重随机重新分配;\n每名玩家每局最多成功转职的次数由下方「转职-次数」决定;\n若没有可转的职业(其余职业名额都已满)则本次不生效、不消耗次数。");
				ReclassItem = BindL(config, "通用", "转职-触发物品", "Mushroom Glow", "食用哪种物品可触发转职(填物品预制体名)。默认 Mushroom Glow = 诡异菇。\n填错或留空则转职机制不会触发。");
				ReclassCount = BindL(config, "通用", "转职-次数", 1000, "每名玩家每局最多可成功转职的次数(默认 1000,等同于不限制)。\n每次成功转职消耗 1 次;没有可转职业时不消耗次数。\n注意:真正的限制通常是「其他职业名额」—— 每次转职都会在**尚未占满**的其他职业里按权重重抽,\n所有职业都满了就会提示「附近没有可转的其他职业」且不消耗次数。\n设为 0 等价于关闭转职。每局开始时重置。");
				ReclassEmoteEnabled = BindL(config, "通用", "转职-表情启用", defaultValue: true, "是否允许玩家通过做「转职-表情」来转职(默认**开启**)。\n开启后:玩家本人做出指定表情即可立即转职一次,效果与食用「转职-触发物品」完全一致,\n并与物品转职**共用**上面的「转职-次数」(每次成功都消耗 1 次)。\n只在玩家**存活、未昏迷、未死亡**时生效;主机为职业效果强制播放的表情不会触发转职。\n注意:「不行不行」是高频沟通表情,容易误触消耗掉本局唯一的转职机会 ——\n不想要时设为 false,或把「转职-次数」调大。");
				ReclassEmoteName = BindL(config, "通用", "转职-表情", ConfigLocalization.UseEnglish ? "No No" : "不行不行", "触发转职的表情。可填「不行不行」(默认)这类显示名,也可直接填原版动画名\n(如 A_Scout_Emote_Nono)。大小写不敏感;留空则表情转职不会触发。\n为避免与职业效果冲突,该值**不允许**是跳舞 / 后空翻 / 装死等会被主机强制播放的表情\n(A_Scout_Emote_Dance / Dance1 / Dance2 / BackFlip / PlayDead),命中时表情转职自动失效。");
				NightHeatEnabled = BindL(config, "通用", "夜晚抵消寒冷", defaultValue: true, "是否抵消「天阶 5 及以上难度」夜晚的寒冷(夜晚每 5 秒 +2.5 寒冷)。\n原理:该夜寒由原版在**每个玩家自己的客户端上**结算,主机无法直接关闭,\n因此改为由主机周期性给所有存活玩家 +「炎热」——原版里炎热与寒冷互为抵消。\n仅在「夜晚 + 天阶>=5(或自定义局开启冷夜)+ 尚未走完 3 个检查点」时生效,条件与原版一致。\n默认 true(开启)。设为 false 恢复原版夜寒。");
				NightHeatIntervalSeconds = BindL(config, "通用", "夜晚抵消寒冷-间隔", 5f, "每隔多少秒下发一次「炎热」(秒)。默认 5,与原版夜寒的积累节奏一致。\n设为 0 或负数关闭本效果。");
				NightHeatAmount = BindL(config, "通用", "夜晚抵消寒冷-量", 0.025f, "每次下发的「炎热」量(0.025 = 2.5 点)。\n默认 0.025:满夜时原版正好每 5 秒 +2.5 寒冷(0.005/秒),因此该默认值可完全抵消。\n若同时受其它寒冷来源影响,可适当调大。");
			}
		}

		public class ExplorerConfig
		{
			public ConfigEntry<bool> Enabled { get; private set; }

			public ConfigEntry<int> MaxPlayers { get; private set; }

			public ConfigEntry<int> MinPlayers { get; private set; }

			public ConfigEntry<int> Weight { get; private set; }

			public ConfigEntry<int> BackpackItemCount { get; private set; }

			public ConfigEntry<int> RopeSpoolWeight { get; private set; }

			public ConfigEntry<int> ClimbingSpikeWeight { get; private set; }

			public ConfigEntry<int> ScoutCookiesWeight { get; private set; }

			public ConfigEntry<int> ChainShooterWeight { get; private set; }

			public ConfigEntry<int> RescueHookWeight { get; private set; }

			public ConfigEntry<int> AntiZookaWeight { get; private set; }

			public ConfigEntry<float> HungerIntervalSeconds { get; private set; }

			public ConfigEntry<float> HungerPerTick { get; private set; }

			public ConfigEntry<float> SprintIntervalSeconds { get; private set; }

			public ConfigEntry<float> DrowsyClearAmount { get; private set; }

			public void Initialize(ConfigFile config)
			{
				Enabled = BindL(config, "先驱", "启用职业", defaultValue: true, "是否启用先驱职业。");
				MaxPlayers = BindL(config, "先驱", "最大人数", 1, "先驱最多分配给几名玩家。设为 0 表示不限制。");
				MinPlayers = BindL(config, "先驱", "最小人数", 0, "先驱最少分配给几名玩家。人数不足时会优先补齐该职业。");
				Weight = BindL(config, "先驱", "抽取权重", 10, "随机分配时先驱被抽中的权重。");
				BackpackItemCount = BindL(config, "先驱", "背包物品数", 4, "开路:每次进入新关卡时直接装备到该玩家身上的背包内装入几件物品(背包容量 4)。\n物品在下方物品池中按权重「独立随机」抽取,允许重复。");
				RopeSpoolWeight = BindL(config, "先驱", "物品权重-绳索", 10, "物品池权重:绳索 RopeSpool。");
				ClimbingSpikeWeight = BindL(config, "先驱", "物品权重-岩钉", 7, "物品池权重:岩钉 ClimbingSpike。");
				ScoutCookiesWeight = BindL(config, "先驱", "物品权重-童军饼干", 2, "物品池权重:童军饼干 ScoutCookies。");
				ChainShooterWeight = BindL(config, "先驱", "物品权重-锁链发射器", 2, "物品池权重:锁链发射器 ChainShooter。");
				RescueHookWeight = BindL(config, "先驱", "物品权重-救援抓钩", 2, "物品池权重:救援抓钩 RescueHook。");
				AntiZookaWeight = BindL(config, "先驱", "物品权重-反重力炮", 1, "物品池权重:反重力炮 AntiZooka。");
				HungerIntervalSeconds = BindL(config, "先驱", "高代谢-饥饿间隔", 20f, "高代谢:每隔多少秒给先驱注入一次饥饿(秒)。默认 20。\n该注入会直接生效并清空原版的饥饿累积器,因此会「覆盖替换」游戏默认的饥饿自然增长。");
				HungerPerTick = BindL(config, "先驱", "高代谢-每次饥饿", 0.025f, "高代谢:每次注入的饥饿量。0.025 = 显示 2.5 点(引擎最小有效步长)。");
				SprintIntervalSeconds = BindL(config, "先驱", "高代谢-加速间隔", 40f, "高代谢:每隔多少秒让先驱获得一次加速(秒)。默认 40。\n加速通过让先驱「饮用」一瓶能量饮料实现;时长由游戏内能量饮料自身决定(约 10 秒),\n仅房主安装时无法改写该时长,实际值会打印到日志。");
				DrowsyClearAmount = BindL(config, "先驱", "高代谢-加速后清除困倦", 0.25f, "高代谢:加速效果结束时,为先驱清除的困倦值(0.25 = 25 点)。\n即抵消饮用能量饮料在加速结束时会附加的困倦效果。设为 0 关闭。");
			}
		}

		public class LeaderConfig
		{
			public ConfigEntry<bool> Enabled { get; private set; }

			public ConfigEntry<int> MaxPlayers { get; private set; }

			public ConfigEntry<int> MinPlayers { get; private set; }

			public ConfigEntry<int> Weight { get; private set; }

			public ConfigEntry<float> InspireIntervalSeconds { get; private set; }

			public ConfigEntry<float> InspireRadius { get; private set; }

			public ConfigEntry<float> InspireExtraStamina { get; private set; }

			public ConfigEntry<float> InspireHungerAmount { get; private set; }

			public ConfigEntry<float> InspireHealAmount { get; private set; }

			public ConfigEntry<float> InspireDrowsyIntervalSeconds { get; private set; }

			public ConfigEntry<float> InspireDrowsyRadius { get; private set; }

			public ConfigEntry<float> InspireDrowsyClearAmount { get; private set; }

			public ConfigEntry<float> InitCurseAmount { get; private set; }

			public ConfigEntry<float> InitPetrifyAmount { get; private set; }

			public ConfigEntry<float> InitExtraStamina { get; private set; }

			public ConfigEntry<float> CursePerDeath { get; private set; }

			public ConfigEntry<float> PetrifyPerDeath { get; private set; }

			public ConfigEntry<float> CurseReducePerAlive { get; private set; }

			public ConfigEntry<bool> CurseReduceIncludeLeader { get; private set; }

			public ConfigEntry<float> MoraleCurseAmount { get; private set; }

			public ConfigEntry<float> MoraleExtraStamina { get; private set; }

			public ConfigEntry<float> MoralePetrifyAmount { get; private set; }

			public ConfigEntry<int> DivineExtraMystical { get; private set; }

			public ConfigEntry<string> DivineItemPool { get; private set; }

			public ConfigEntry<int> RevivePetrifyCap { get; private set; }

			public void Initialize(ConfigFile config)
			{
				Enabled = BindL(config, "领袖", "启用职业", defaultValue: true, "是否启用领袖职业。");
				MaxPlayers = BindL(config, "领袖", "最大人数", 1, "领袖最多分配给几名玩家。设为 0 表示不限制。");
				MinPlayers = BindL(config, "领袖", "最小人数", 0, "领袖最少分配给几名玩家。人数不足时会优先补齐该职业。默认 0(不强制)。");
				Weight = BindL(config, "领袖", "抽取权重", 10, "随机分配时领袖被抽中的权重。");
				InspireIntervalSeconds = BindL(config, "领袖", "号召-触发间隔", 180f, "领袖号召:自动触发的间隔(秒),默认 180 = 3 分钟。每次触发对半径内所有人(含领袖自己)加额外体力、恢复饥饿与损伤。");
				InspireRadius = BindL(config, "领袖", "号召-作用半径", 30f, "领袖号召:作用半径(米)。");
				InspireExtraStamina = BindL(config, "领袖", "号召-额外体力", 0.25f, "领袖号召:额外体力(0.25 = 25)。");
				InspireHungerAmount = BindL(config, "领袖", "号召-恢复饥饿", 0.1f, "领袖号召:恢复的饥饿值(0.1 = 10 点)。");
				InspireHealAmount = BindL(config, "领袖", "号召-恢复损伤", 0.05f, "领袖号召:每次触发为半径内所有人(含领袖自己)恢复的损伤(0.05 = 5 点)。设为 0 关闭。");
				InspireDrowsyIntervalSeconds = BindL(config, "领袖", "号召-困倦间隔", 60f, "领袖号召·清醒:单独计时的困倦清除间隔(秒),默认 60 = 1 分钟。\n与上方「号召-触发间隔」相互独立(体力/饥饿/损伤按 3 分钟一次,困倦按 1 分钟一次)。");
				InspireDrowsyRadius = BindL(config, "领袖", "号召-困倦半径", 20f, "领袖号召·清醒:作用半径(米),默认 20。");
				InspireDrowsyClearAmount = BindL(config, "领袖", "号召-清除困倦", 0.3f, "领袖号召·清醒:每次为半径内所有人(含领袖自己)清除的困倦(0.30 = 30 点)。");
				InitCurseAmount = BindL(config, "领袖", "宿命-初始诅咒", 0.2f, "宿命:开局初始诅咒(0.20 = 20 点)。");
				InitPetrifyAmount = BindL(config, "领袖", "宿命-初始石化", 0.2f, "宿命:开局初始石化(0.20 = 20 点)。");
				InitExtraStamina = BindL(config, "领袖", "宿命-初始额外体力", 0.3f, "宿命:开局初始额外体力(0.30 = 30 点)。");
				CursePerDeath = BindL(config, "领袖", "圣洁-每死亡者诅咒", 0.05f, "圣洁:使用古老雕像时,每有 1 人死亡则领袖增加的诅咒(0.05 = 5 点)。");
				PetrifyPerDeath = BindL(config, "领袖", "圣洁-每死亡者石化", 0.1f, "圣洁:使用古老雕像时,每有 1 人死亡则领袖增加的石化(0.10 = 10 点)。");
				CurseReducePerAlive = BindL(config, "领袖", "圣洁-每存活者消除诅咒", 0.025f, "圣洁:使用古老雕像时,每有 1 人存活则领袖消除的诅咒(0.025 = 2.5 点)。\n存活人数是否含领袖本人,由下方「圣洁-存活计数含领袖」决定。\n注意:游戏状态的「最小步长」为 0.025(STATUS_INCREMENT),低于该值不会生效,\n0.025(= 显示 2.5 点)即引擎允许的最小有效变动量。");
				CurseReduceIncludeLeader = BindL(config, "领袖", "圣洁-存活计数含领袖", defaultValue: false, "圣洁:计算「每有 1 人存活则领袖消除诅咒」时,存活人数是否把领袖本人也计入。\nfalse(默认)= 不含领袖本人;true = 含领袖本人(即领袖活着时自己多消一份)。");
				MoraleCurseAmount = BindL(config, "领袖", "士气-诅咒", 0.1f, "士气:领袖死亡时,对每名存活玩家施加的诅咒(0.10 = 10 点);领袖复活时等量消除。");
				MoraleExtraStamina = BindL(config, "领袖", "士气-额外体力", 0.3f, "士气:领袖死亡时,对每名存活玩家施加的额外体力(0.30 = 30 点)。");
				MoralePetrifyAmount = BindL(config, "领袖", "士气-石化", 0.1f, "士气:领袖死亡时,对每名存活玩家施加的石化(0.10 = 10 点);领袖复活时等量消除。\n另外领袖死亡时,存活玩家还会获得一次持续约 10 秒的加速(能量饮料效果)。");
				DivineExtraMystical = BindL(config, "领袖", "神启-额外神秘物品", 1, "神启:全员存活时使用古老雕像,为领袖额外发放的神秘物品数量。\n优先直接塞进领袖的 3 个物品栏空位;三个物品栏都满时改为在篝火附近生成并固定(不会滚落)。设为 0 关闭。");
				DivineItemPool = BindL(config, "领袖", "神启-物品池", "BookOfBones,PandorasBox,ScoutEffigy,Lantern_Faerie,Bugle_Magic,Cure-All,RitualDagger,AntiZooka,Anti-Rope Spool,RopeShooterAnti,AncientIdol,Cursed Skull", "神启:额外神秘物品的「白名单」物品池(英文逗号分隔的预制体名,从名单中等概率随机抽取)。\n默认只包含真正的神秘物品;留空则会退化为「按原版 Mystical 标签筛选」,\n该筛选会把未实装物品(例如 ClimbingChalk)也算进来,不建议留空。");
				RevivePetrifyCap = BindL(config, "领袖", "复活继承-石化上限", 90, "领袖复活后继承死亡瞬间的石化值,但最高只继承到该值(0~100)。\n例:死亡时石化 100 → 复活后石化 = 90;死亡时石化 20 → 复活后石化 = 20。\n该上限用于防止「满石化死亡 → 复活即满石化 → 无限死亡循环」。");
			}
		}

		public class LogisticianConfig
		{
			public ConfigEntry<bool> Enabled { get; private set; }

			public ConfigEntry<int> MaxPlayers { get; private set; }

			public ConfigEntry<int> MinPlayers { get; private set; }

			public ConfigEntry<int> Weight { get; private set; }

			public ConfigEntry<float> AirdropIntervalSeconds { get; private set; }

			public ConfigEntry<int> AirdropCount { get; private set; }

			public ConfigEntry<string> AirdropPool { get; private set; }

			public ConfigEntry<string> AirdropBlacklist { get; private set; }

			public ConfigEntry<int> InitialBackpacks { get; private set; }

			public ConfigEntry<int> BackpackWeight { get; private set; }

			public ConfigEntry<int> JetpackWeight { get; private set; }

			public ConfigEntry<float> InitCurseAmount { get; private set; }

			public ConfigEntry<float> InitPetrifyAmount { get; private set; }

			public ConfigEntry<int> RevivePetrifyFixed { get; private set; }

			public void Initialize(ConfigFile config)
			{
				Enabled = BindL(config, "后勤", "启用职业", defaultValue: true, "是否启用后勤职业。");
				MaxPlayers = BindL(config, "后勤", "最大人数", 1, "后勤最多分配给几名玩家。设为 0 表示不限制。");
				MinPlayers = BindL(config, "后勤", "最小人数", 0, "后勤最少分配给几名玩家。人数不足时会优先补齐该职业。");
				Weight = BindL(config, "后勤", "抽取权重", 10, "随机分配时后勤被抽中的权重。");
				AirdropIntervalSeconds = BindL(config, "后勤", "空投-触发间隔", 90f, "紧急空投:自动触发的间隔(秒),默认 90 = 1.5 分钟。");
				AirdropCount = BindL(config, "后勤", "空投-每次数量", 1, "紧急空投:每次发放的物品数量。");
				AirdropPool = BindL(config, "后勤", "空投-物品池", "RopeSpool,ClimbingSpike,Lantern,Torch,Parasol,Flare,RescueHook,Glider,Parachute,RopeShooter,ChainShooter,PortableStovetopItem,Balloon,BalloonBunch,Antidote,AloeVera,Heat Pack,Sunscreen,Bandages,MedicinalRoot,FirstAidKit,ScoutCookies,Energy Drink,Sports Drink,Compass,Pirate Compass,Guidebook,Binoculars,Candle,Frisbee,Stone,ScoutCannonItem,BingBong", "紧急空投:可掉落物品的预制体名列表(英文逗号分隔)。留空表示使用全部非禁用物品。\n名称中含空格的要保留原样(如 Energy Drink)。");
				AirdropBlacklist = BindL(config, "后勤", "空投-物品黑名单", "Cheat Compass,Cheat Compass 1,Passport,RescueHook_Infinite,Amulet_Healing,Amulet_Supe