Decompiled source of RollOfTheScout v1.1.2
plugins/com.peakmodding.RollOfTheScout.dll
Decompiled a week 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.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; 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 RollOfTheScout.Shared; 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.1.2.0")] [assembly: AssemblyInformationalVersion("1.1.2")] [assembly: AssemblyProduct("com.peakmodding.RollOfTheScout")] [assembly: AssemblyTitle("RollOfTheScout")] [assembly: AssemblyVersion("1.1.2.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; } TMP_FontAsset val = null; TMP_FontAsset val2 = null; TMP_FontAsset val3 = null; try { FontFallbackSwapper instance = FontFallbackSwapper.instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.mainBaseFont != (Object)null) { val = instance.mainBaseFont; } } catch { } try { TMP_FontAsset[] array = Resources.FindObjectsOfTypeAll<TMP_FontAsset>(); foreach (TMP_FontAsset val4 in array) { if (!((Object)(object)val4 == (Object)null) && HasCjk(val4)) { if ((Object)(object)val == (Object)null && ((Object)val4).name != null && ((Object)val4).name.IndexOf("Daruma", StringComparison.OrdinalIgnoreCase) >= 0) { val = val4; } if ((Object)(object)val2 == (Object)null && HasChar(val4, 'A')) { val2 = val4; } if ((Object)(object)val3 == (Object)null && HasChar(val4, 'A') && LooksThin(val4)) { val3 = val4; } } } } catch { } if ((Object)(object)val3 == (Object)null && (Object)(object)val2 == (Object)null) { try { PlayerName[] array2 = Resources.FindObjectsOfTypeAll<PlayerName>(); foreach (PlayerName val5 in array2) { if (!((Object)(object)val5 == (Object)null) && !((Object)(object)val5.text == (Object)null) && !((Object)(object)((TMP_Text)val5.text).font == (Object)null) && HasCjk(((TMP_Text)val5.text).font) && HasChar(((TMP_Text)val5.text).font, 'A')) { val2 = ((TMP_Text)val5.text).font; if ((Object)(object)val3 == (Object)null && LooksThin(((TMP_Text)val5.text).font)) { val3 = ((TMP_Text)val5.text).font; } break; } } } catch { } } if ((Object)(object)val3 != (Object)null) { return _resolved = val3; } if ((Object)(object)val2 != (Object)null) { return _resolved = val2; } if ((Object)(object)val != (Object)null && HasCjk(val) && HasChar(val, 'A')) { return _resolved = val; } try { TMP_FontAsset defaultFontAsset = TMP_Settings.defaultFontAsset; if ((Object)(object)defaultFontAsset != (Object)null && HasCjk(defaultFontAsset)) { return _resolved = defaultFontAsset; } } catch { } return null; } private static bool LooksThin(TMP_FontAsset f) { if ((Object)(object)f == (Object)null || string.IsNullOrEmpty(((Object)f).name)) { return false; } string name = ((Object)f).name; if (name.IndexOf("Daruma", StringComparison.OrdinalIgnoreCase) >= 0) { return false; } if (name.IndexOf("Noto", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("Sans", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("SourceHan", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("Source Han", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("CJK", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("Hei", StringComparison.OrdinalIgnoreCase) < 0) { return name.IndexOf("Gothic", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static bool HasChar(TMP_FontAsset font, char c) { try { return (Object)(object)font != (Object)null && font.HasCharacter(c, true, false); } catch { return false; } } 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); PositionToast(); } } 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); PositionToast(); } } } 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; PositionToast(); } 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; PositionToast(); } } 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; PositionToast(); return; } if (version < 0 || version != intro.LastVersion) { intro.LastVersion = version; SetBlock(intro, Join(lines, title), intro.Bold); } PositionToast(); } public static void SetClassIntroHidden() { if (Ready) { SetBlock(_intro, null, _intro.Bold); _intro.LastVersion = int.MinValue; PositionToast(); } } private static void PositionToast() { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) Block toast = _toast; if (toast != null && !((Object)(object)toast.Root == (Object)null)) { float num = 12f; if (_intro != null && (Object)(object)_intro.Root != (Object)null && ((Component)_intro.Root).gameObject.activeSelf) { num += _intro.Root.sizeDelta.y + 8f; } toast.Root.anchoredPosition = new Vector2(12f, 0f - num); } } 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 Build(TMP_FontAsset font) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown _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: false); _panel = CreateBlock("ClassPanel", font, rightSide: true, 0.32f, bold: false); _intro = CreateBlock("ClassIntro", font, rightSide: false, 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_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) //IL_01af: 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; ((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>").Append(title).Append("</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 KindTargetedToast = 6; public const byte KindModeSnapshot = 11; public const byte KindModeOff = 12; public const byte KindMilesClientHello = 7; public const byte KindMilesUsageReport = 9; public const byte KindMilesClientReady = 10; public const byte KindMilesGameplayState = 8; public const float ClassTextResendSeconds = 10f; public const float SnapshotIntervalSeconds = 5f; 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 BroadcastTargetedToast(int actorNumber, string text) { if (ShouldBroadcast() && actorNumber > 0 && !string.IsNullOrEmpty(text)) { Send(6, actorNumber + "\t" + SanitizeToast(text)); } } 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', ' '); } private static string SanitizeToast(string text) { if (string.IsNullOrEmpty(text)) { return string.Empty; } return text.Replace('\t', ' '); } public static void BroadcastToast(string text) { if (ShouldBroadcast() && !string.IsNullOrEmpty(text)) { Send(2, text); } } public static void BroadcastModeSnapshot(IList<KeyValuePair<int, int>> actorStatusPairs) { if (!PhotonNetwork.InRoom || !PhotonNetwork.IsMasterClient) { return; } if (actorStatusPairs == null || actorStatusPairs.Count == 0) { BroadcastModeOff(); return; } StringBuilder stringBuilder = new StringBuilder(actorStatusPairs.Count * 8); for (int i = 0; i < actorStatusPairs.Count; i++) { if (i > 0) { stringBuilder.Append(';'); } stringBuilder.Append(actorStatusPairs[i].Key).Append('=').Append(actorStatusPairs[i].Value); } SendForced(11, stringBuilder.ToString()); } public static void BroadcastModeOff() { if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient) { SendForced(12, "0"); } } public static void BroadcastModeToast(string text) { if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && !string.IsNullOrEmpty(text)) { SendForced(2, text); } } public static void SendMilesGameplayState(int actorNumber, string data) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) if (!PhotonNetwork.InRoom || !PhotonNetwork.IsMasterClient || actorNumber <= 0 || string.IsNullOrEmpty(data)) { return; } try { object[] obj = new object[3] { "com.peakmodding.RollOfTheScout", (byte)8, data }; RaiseEventOptions val = new RaiseEventOptions(); val.TargetActors = new int[1] { actorNumber }; PhotonNetwork.RaiseEvent((byte)197, (object)obj, val, SendOptions.SendReliable); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RollOfTheScout] 残魂本机配置同步失败:" + ex.Message)); } } 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 }; if (!PhotonNetwork.RaiseEvent((byte)197, (object)array, new RaiseEventOptions { Receivers = (ReceiverGroup)0 }, SendOptions.SendReliable)) { Plugin.Log.LogWarning((object)$"[RollOfTheScout] 职业显示广播未被 Photon 接受(kind={kind},payload={array.Length} 段)。"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)$"[RollOfTheScout] 职业显示网络广播失败(kind={kind}):{ex.Message}"); } } private static void SendForced(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 }; if (!PhotonNetwork.RaiseEvent((byte)197, (object)array, new RaiseEventOptions { Receivers = (ReceiverGroup)0 }, SendOptions.SendReliable)) { Plugin.Log.LogWarning((object)$"[RollOfTheScout] 玩法模式广播未被 Photon 接受(kind={kind},payload={array.Length} 段)。"); } } 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 => "[职业介绍 / Class Intro] "; public static string Keywords(int code) { string text = KeywordsEn(code); string text2 = KeywordsZh(code); if (string.IsNullOrEmpty(text) && string.IsNullOrEmpty(text2)) { return string.Empty; } if (string.IsNullOrEmpty(text2)) { return "Keywords: " + text; } if (string.IsNullOrEmpty(text)) { return "关键词:" + text2; } return "Keywords: " + text + "\n关键词:" + text2; } public static string Intro(int code) { string text = IntroEn(code); string text2 = IntroZh(code); if (string.IsNullOrEmpty(text)) { return text2; } if (string.IsNullOrEmpty(text2)) { return text; } return text + "\n" + text2; } public static string KeywordsLine(int code) { return Keywords(code); } public static string IntroLine(int code) { return Intro(code); } 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 => "传送、曲迁菇、负面状态、周期检测", 32 => "石化、石头、动态增益、拾取转化、随机效果、范围守护", 33 => "工具转化、随机负面、食物炼化、周期强力炼制、背人炼成", 34 => "周期范围发糖、满物品栏捣蛋、拾取转化、吃糖惩罚", _ => 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 => "【并非和平】右键拉人变为踢人;每踢中一名玩家+5额外体力,踢空则-5额外体力并+20困倦;开局自带诅咒。", 20 => "【负重前行】开局自带背包;负重过半达标后,周期获得加速与额外体力。", 21 => "【火线救援】队友昏迷将自动传送并获得一次无敌,背起再获得一次加速与大量额外体力;背起玩家时治疗双方;有玩家死亡会获得石化惩罚。", 22 => "【精致防晒】每关获得三瓶防晒喷雾,强制覆盖物品栏;获得防晒奖励额外体力,每次用完防晒喷雾会额外奖励额外体力并积累石化。", 23 => "【单人模式】离人够远时周期奖励额外体力;靠近他人持续困倦。", 24 => "【重火力者】每关获得童子军大炮;定时获得童子军大炮;受伤就会获得炸药,且物品栏满时会直接塞入主手。", 25 => "【演奏乐队】拾取物品概率变乐器,库存与演奏可范围奖励额外体力;库存集齐三种乐器有额外加速奖励,且给予他人有低概率将其转职。", 26 => "【领队残魂】初始诅咒锁定,仅能通过获取童军宝石道具降低;使用童军宝石道具有专属特殊增益;禁用童军宝石道具以外物品,拾取物品直接消失;即时传送救援,不可放弃他人;集齐所有童军宝石道具有额外奖励;抵达天底解放灵魂全员奖励。", 27 => "【蠕动之区】攀爬时高频强制脱手;靠拾取分解垃圾物品、获取积攒早起虫儿积累收益;感染式转职,周期对附近施加孢子,过半会使他人强制同化,并获得早起虫儿;同类聚集有加速与无限体力效果奖励;自身免疫孢子;食用早期虫儿有诅咒惩罚。", 28 => "【太空漂流】身上定时绑上气球;无法拾取气球/一束气球;拾取物品概率变为火箭背包/喷气背包;只吃包装食品或熟食,否则中毒;对冷热有一定抗性,但部分负面状态会额外加重。", 29 => "【大果成瘾】定时获得青/紫荔莓;食用青荔莓获得冲刺效果、紫荔莓则给无限体力;食用其他食物加困倦;每次进食固定损伤;他人多次连吃荔莓会被强制同化。", 30 => "【代码之力】定时获得随机虫类,获得时大概率中毒;额外体力溢出会取模重置;夜晚周期获得加速;独行会周期清负面;无负面且无虫时会复制在场他人职业效果,并持续生效一段时间。", 31 => "【混乱传送】周期检测自身状态,若检测的负面状态相比上一次增加,则与随机一名其他玩家互换位置,否则获得曲迁菇;拾取物品有较高概率直接消失并触发潘多拉;饮用运动饮料或万灵药、接触水体都会带来额外惩罚;积攒过多曲迁菇会受到惩罚。", 32 => "【活体石像】站立不动会持续积蓄石化,移动则缓慢消退;自身石化越高,增益效果越强;若石化偏低,白天会畏光叠加困倦;周期内石化达到峰值时会强制破壳,触发一次性强力增益,且能影响周围;拾取食物会变石头,而石头可能带来随机增益。", 33 => "【等价炼成】拾取工具时获得随机负面并转化为随机物品;拾取可食用物品会转化为负面清除与额外体力,每12次会有特殊奖励;每炼制出12石头将触发一次特殊强力炼制;背起玩家会将其变为骷髅,或将其炼化为尸。", 34 => "【万圣敲门】定时给范围内的玩家发糖,成功使自身获得额外体力;若对方物品栏已满则捣蛋:随机抢走一件物品或连施潘多拉;拾取食物会变糖,但吃糖有蛀牙的损伤惩罚。", _ => 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 => "Debuff conversion · Penalty without debuffs · Periodic debuff tolerance", 9 => "Poison/spore resistance · Natural food only · Periodic pink banana · Tools disabled", 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 => "Teleport · Warp Fungus · Negative statuses · Periodic checks", 32 => "Petrify · Stones · Dynamic buffs · Pickup conversion · Random effects · Area guard", 33 => "Tool transmutation · Random debuff · Food conversion · Forced periodic refining · Carrying taboo", 34 => "Periodic area candy · Trick on full inventories · Pickup conversion · Sugar penalty", _ => 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】 periodic strong area buffs that grant players extra stamina, heal injury, restore hunger and reduce drowsiness; 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 => "【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.", 9 => "【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.", 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 or 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; each hit grants +5 extra stamina, while a miss costs 5 extra stamina and adds 20 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 => "【Chaos Teleport】periodically checks your own status; if the selected negative status increased, swap positions with a random other player; otherwise gain a Warp Fungus; picked-up items may vanish and trigger Pandora; sports drinks, Cure-All and touching water add extra punishment; hoarding too many Warp Fungi is punished.", 32 => "【Living Statue】Standing still builds petrify while moving slowly sheds it; the higher your petrify, the stronger your buffs; when petrify is low, daylight adds drowsiness; when petrify peaks within a cycle you are forced to break the shell, triggering a one-time powerful buff that also affects those around you; food you pick up turns to stone, and stones may bring random buffs.", 33 => "【Equivalent alchemy】picked-up tools gain a random debuff and are turned into random items; picked-up edible items are converted into debuff relief and extra stamina, with a special reward every 12 triggers; every 12 stones refined triggers one special powerful transmutation; carrying a player turns them into a skeleton, or refines them into a corpse.", 34 => "【Halloween knocker】candy to players in range on a timer, granting you extra stamina when it lands; a full inventory gets tricked — one item is snatched or Pandora strikes repeatedly; edible pickups turn into candy, but eating candy costs injury from cavities.", _ => 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>(); private static readonly string[] _bilingualNames = new string[64]; 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.ChaosEnderman.Enabled.Value) { int num32 = MaxOf(ClassType.ChaosEnderman); if (num32 <= 0) { return int.MaxValue; } num += num32; } if (ModConfig.Gargoyle.Enabled.Value) { int num33 = MaxOf(ClassType.Gargoyle); if (num33 <= 0) { return int.MaxValue; } num += num33; } if (ModConfig.Alchemist.Enabled.Value) { int num34 = MaxOf(ClassType.Alchemist); if (num34 <= 0) { return int.MaxValue; } num += num34; } if (ModConfig.Trickster.Enabled.Value) { int num35 = MaxOf(ClassType.Trickster); if (num35 <= 0) { return int.MaxValue; } num += num35; } 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; } public bool ForceSet(Character character, ClassType type) { 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.ChaosEnderman.Enabled.Value) { list.Add(ClassType.ChaosEnderman); } if (ModConfig.Gargoyle.Enabled.Value) { list.Add(ClassType.Gargoyle); } if (ModConfig.Alchemist.Enabled.Value) { list.Add(ClassType.Alchemist); } if (ModConfig.Trickster.Enabled.Value) { list.Add(ClassType.Trickster); } 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.ChaosEnderman => Mathf.Max(0, ModConfig.ChaosEnderman.Weight.Value), ClassType.Gargoyle => Mathf.Max(0, ModConfig.Gargoyle.Weight.Value), ClassType.Alchemist => Mathf.Max(0, ModConfig.Alchemist.Weight.Value), ClassType.Trickster => Mathf.Max(0, ModConfig.Trickster.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 => PandoraModeBridge.EffectivePandoraMaxPlayers(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.ChaosEnderman => Mathf.Max(0, ModConfig.ChaosEnderman.MaxPlayers.Value), ClassType.Gargoyle => Mathf.Max(0, ModConfig.Gargoyle.MaxPlayers.Value), ClassType.Alchemist => Mathf.Max(0, ModConfig.Alchemist.MaxPlayers.Value), ClassType.Trickster => Mathf.Max(0, ModConfig.Trickster.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.ChaosEnderman => Mathf.Max(0, ModConfig.ChaosEnderman.MinPlayers.Value), ClassType.Gargoyle => Mathf.Max(0, ModConfig.Gargoyle.MinPlayers.Value), ClassType.Alchemist => Mathf.Max(0, ModConfig.Alchemist.MinPlayers.Value), ClassType.Trickster => Mathf.Max(0, ModConfig.Trickster.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.ChaosEnderman => "混乱末影人", ClassType.Gargoyle => "石像鬼", ClassType.Alchemist => "炼金术士", ClassType.Trickster => "捣蛋鬼", _ => "无职业", }; } public static string DescribeEnglish(ClassType type) { return type switch { ClassType.Explorer => "Explorer", ClassType.Leader => "Leader", ClassType.Logistician => "Logistician", ClassType.Doctor => "Doctor", ClassType.Assassin => "Assassin", ClassType.Pandora => "Pandora's Child", ClassType.Necromancer => "Necromancer", ClassType.M => "M", ClassType.Savage => "Savage", ClassType.Scout => "Scout", ClassType.StandardBearer => "StandardBearer", ClassType.Dancer => "Dancer", ClassType.Daihao => "Daihao", ClassType.Saint => "Saint", ClassType.Mage => "Mage", ClassType.Challenger => "Challenger", ClassType.MasterChef => "MasterChef", ClassType.Cook => "Cook", ClassType.Pacifist => "Pacifist", ClassType.Hamster => "Hamster", ClassType.Firefighter => "Firefighter", ClassType.Narcissist => "Narcissist", ClassType.LoneWolf => "LoneWolf", ClassType.Gunner => "Gunner", ClassType.Band => "Band", ClassType.MilesSoul => "MilesSoul", ClassType.Qu => "Qu", ClassType.Astronaut => "Astronaut", ClassType.Langwang => "Langwang", ClassType.Programmer => "Programmer", ClassType.ChaosEnderman => "ChaosEnderman", ClassType.Gargoyle => "Gargoyle", ClassType.Alchemist => "Alchemist", ClassType.Trickster => "Trickster", _ => "None", }; } public static string DescribeBilingual(ClassType type) { if (type > ClassType.None && (int)type < _bilingualNames.Length && _bilingualNames[(int)type] != null) { return _bilingualNames[(int)type]; } string text = ((type <= ClassType.None) ? "None · 无职业" : (DescribeEnglish(type) + " · " + Describe(type))); if (type > ClassType.None && (int)type < _bilingualNames.Length) { _bilingualNames[(int)type] = text; } return text; } } 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, ChaosEnderman, Gargoyle, Alchemist, Trickster } 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 ConfigEntry<bool> HalloweenPumpkinHat { 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, "通用", "职业显示-模式", "1", "职业分配面板的显示方式(1 / 2 / 3):\n1 = 按住该键时显示,松开立即隐藏;\n2 = 按一下显示 5 秒后自动隐藏;\n3 = 每按一次在「显示 / 隐藏」之间切换。\n默认 1。旧版的文字值(按住显示 / 按下显示 / 按下切换、HoldToShow 等)仍兼容。"); 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: false, "是否把「头顶玩家名称」改写成「【职业】昵称」,例如「Rhys」→「【先驱】Rhys」。\n「【职业】」用该玩家进入游戏时的皮肤色着色;「昵称」维持原版颜色不变。\n⚠ **默认关闭**:该功能由「头顶名称刷新补丁」每帧、每个玩家执行;若同时装有其它的头顶名称模组\n(例如 AlwaysDisplayPlayerName),双方会每帧互相改写文本,导致明显掉帧。\n⚠ 头顶名称是**每个客户端本地**的 UI,因此本开关只影响**本机屏幕**上看到的名字:\n · 主机:显示本局账本里的职业;\n · 装了本模组或附属模组的客机:显示主机广播过来的职业(见「职业显示-网络同步」);\n · 未装任何模组的客机:看不到任何前缀(仍显示原版昵称)。\n默认 false(关闭)。"); ReclassEnabled = BindL(config, "通用", "转职-启用", defaultValue: true, "转职机制总开关。true=玩家食用下方「转职-触发物品」后可更换职业;false=完全关闭。\n转职会在「当前未占满名额的其他职业」中按权重随机重新分配;\n每名玩家每局最多成功转职的次数由下方「转职-次数」决定;\n若没有可转的职业(其余职业名额都已满)则本次不生效、不消耗次数。"); ReclassItem = BindL(config, "通用", "转职-触发物品", "Mushroom Glow", "食用哪种物品可触发转职(填物品预制体名)。默认 Mushroom Glow = 诡异菇。\n填错或留空则转职机制不会触发。"); ReclassCount = BindL(config, "通用", "转职-次数", 1, "每名玩家每局最多可成功转职的次数(默认 1)。\n每次成功转职消耗 1 次;没有可转职业时不消耗次数。\n设为 0 等价于关闭转职。每局开始时重置。"); ReclassEmoteEnabled = BindL(config, "通用", "转职-表情启用", defaultValue: false, "是否允许玩家通过做「转职-表情」来转职(默认**关闭**)。\n开启后:玩家本人做出指定表情即可立即转职一次,效果与食用「转职-触发物品」完全一致,\n并与物品转职**共用**上面的「转职-次数」(每次成功都消耗 1 次)。\n只在玩家**存活、未昏迷、未死亡**时生效;主机为职业效果强制播放的表情不会触发转职。\n注意:「不行不行」是高频沟通表情,默认关闭以避免误触消耗掉本局唯一的转职机会。"); ReclassEmoteName = BindL(config, "通用", "转职-表情", "A_Scout_Emote_Nono", "触发转职的表情。固定填原版动画名(默认 A_Scout_Emote_Nono,即「不行不行」)。\n大小写不敏感;留空则表情转职不会触发。\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若同时受其它寒冷来源影响,可适当调大。"); HalloweenPumpkinHat = BindL(config, "通用", "万圣节-南瓜头", defaultValue: false, "万圣节彩蛋:开启后,房间内所有玩家(含机场大厅、中途加入与退出重进的玩家)的头饰\n会被强制换成南瓜头(帽子编号 27);关闭后恢复为各自原本的头饰。\n开启 / 关闭的切换有 60 秒内置冷却:冷却期间的修改会被**直接忽略**(不会排队到冷却结束后生效),\n如冷却期间改过本项,请在冷却结束后再切换一次。\n玩家做「耸肩」表情(A_Scout_Emote_Shrug)会在「原本帽子 / 南瓜头」之间切换;\n每名玩家该切换有 30 秒内置冷却,冷却期间不做任何检测与判断,也不会延迟到冷却结束后生效。\n仅在主机生效;默认 false(关闭)。"); } } 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, "先驱", "高代