Decompiled source of RollOfTheScoutPerk v1.0.8
com.peakmodding.RollOfTheScout.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
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