Decompiled source of Quickstart v1.0.1
Quickstart.dll
Decompiled a month ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CG.GameLoopStateMachine; using CG.GameLoopStateMachine.GameStates; using CG.Profile; using Client.Utils; using Gameplay.Hub; using Gameplay.Quests; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Photon.Pun; using Photon.Realtime; using PlatformAbstractionLayer; using ResourceAssets; using ToolClasses; using UI; using UI.Core.Audio; using UnityEngine; using UnityEngine.UIElements; using VoidManager; using VoidManager.CustomGUI; using VoidManager.MPModChecks; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Quickstart")] [assembly: AssemblyDescription("Adds a Quickstart option to the main menu that drops you straight into a pilgrimage in the room kind and ship loadout you last used, skipping the hub entirely.")] [assembly: AssemblyProduct("Quickstart")] [assembly: AssemblyCompany("Jack")] [assembly: AssemblyFileVersion("1.0.1.0")] [assembly: ComVisible(false)] [assembly: AssemblyMetadata("ModCategories", "Quality of Life, Client-Side")] [assembly: AssemblyMetadata("MPType", "Client")] [assembly: AssemblyMetadata("ProgressionFlag", "enabled")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.1.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; } } } internal static class MenuKit { private static readonly Dictionary<string, bool> foldouts = new Dictionary<string, bool>(); private static readonly HashSet<string> loggedFailures = new HashSet<string>(); public static void Header(string text) { GUILayout.Space(10f); GUILayout.Label("<b>" + text + "</b>", Array.Empty<GUILayoutOption>()); } public static void Rule() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: 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) GUILayout.Space(6f); Rect rect = GUILayoutUtility.GetRect(1f, 1f, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); Color color = GUI.color; GUI.color = new Color(1f, 1f, 1f, 0.25f); GUI.DrawTexture(rect, (Texture)(object)Texture2D.whiteTexture); GUI.color = color; GUILayout.Space(6f); } public static bool Foldout(string id, string label) { foldouts.TryGetValue(id, out var value); if (GUILayout.Button((value ? "[-] " : "[+] ") + label, Array.Empty<GUILayoutOption>())) { value = !value; foldouts[id] = value; } return value; } public static void DevZone(string id, Action body) { if (!Configs.IsDebugMode) { return; } GUILayout.Space(12f); Rule(); if (!Foldout(id + ".dev", "Developer tools")) { return; } GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); try { body(); } catch (Exception ex) { GUILayout.Label("Dev section failed: " + ex.Message + " (see log)", Array.Empty<GUILayoutOption>()); if (loggedFailures.Add(id)) { Debug.LogError((object)("[MenuKit] dev zone '" + id + "' threw: " + ex)); } } finally { GUILayout.EndVertical(); } } } internal static class ModFlagPolicy { internal const bool FlagsSession = true; internal const MultiplayerType MPType = (MultiplayerType)20; internal const SessionModificationEffect NetworkSpawnedEffect = (SessionModificationEffect)2; internal const SessionModificationEffect RequiredEffect = (SessionModificationEffect)94; internal const string BuildTag = "enabled"; } internal static class MutatorSlots { private const string TargetType = "Gameplay.Hub.HubMutatorManager"; private const string TargetMethod = "GetMaxMutatorsCount"; internal static ConfigEntry<bool> ExtraSlot; internal static bool Installed; internal static string Status = "not installed"; private static ManualLogSource _log; private static string _tag; internal static int Bonus { get { try { return (ExtraSlot != null && ExtraSlot.Value) ? 1 : 0; } catch { return 0; } } } internal static void Install(ConfigFile config, Harmony harmony, ManualLogSource log, string tag) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown _log = log; _tag = tag; try { ExtraSlot = config.Bind<bool>("Mutators", "ExtraMutatorSlot", true, "Raise the Mutators menu's selection limit by one, so this mod's mutator does not cost one of the vanilla slots. Every installed mod with this setting on adds its own +1. The limit is checked by the host, so the host's setting is the one that counts in a multiplayer room. Takes effect immediately."); if (!Installed) { Type type = AccessTools.TypeByName("Gameplay.Hub.HubMutatorManager"); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "GetMaxMutatorsCount", (Type[])null, (Type[])null)); if (methodInfo == null) { Status = "FAILED - Gameplay.Hub.HubMutatorManager.GetMaxMutatorsCount not found (game update?)"; log.LogError((object)(tag + ": extra mutator slot not installed: " + Status)); } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(MutatorSlots), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Installed = true; Status = "installed"; } } } catch (Exception ex) { Status = "FAILED - " + ex.GetType().Name; log.LogError((object)(tag + ": extra mutator slot not installed, vanilla limit stays: " + ex)); } } private static void Postfix(ref int __result) { try { __result += Bonus; } catch (Exception ex) { if (_log != null) { _log.LogWarning((object)(_tag + ": extra mutator slot postfix failed: " + ex)); } } } internal static void Draw() { if (ExtraSlot == null) { GUILayout.Label("Extra slot : " + Status, Array.Empty<GUILayoutOption>()); return; } bool flag = GUILayout.Toggle(ExtraSlot.Value, " Extra mutator slot (+1 to the selection limit; host's setting counts)", Array.Empty<GUILayoutOption>()); if (flag != ExtraSlot.Value) { ExtraSlot.Value = flag; } if (!Installed) { GUILayout.Label("Extra slot : " + Status, Array.Empty<GUILayoutOption>()); } } } internal static class StatContrib { internal enum Kind { Additive, Multiplicative, Override } private const string SlotKey = "jack.voidcrew.statcontrib.v1"; internal static ManualLogSource Log; private static object[] Root() { object[] array = AppDomain.CurrentDomain.GetData("jack.voidcrew.statcontrib.v1") as object[]; if (array == null) { array = new object[2] { new object(), new Dictionary<object, object>() }; AppDomain.CurrentDomain.SetData("jack.voidcrew.statcontrib.v1", array); } return array; } internal static bool Set(object target, string field, string ownerId, Func<float> getter, Action<float> setter, Kind kind, float value, Func<bool> alive = null) { if (target == null || string.IsNullOrEmpty(field) || string.IsNullOrEmpty(ownerId) || getter == null || setter == null) { Error("StatContrib.Set called with a null argument (field '" + field + "', owner '" + ownerId + "') - ignored."); return false; } if (float.IsNaN(value) || float.IsInfinity(value)) { Error("StatContrib.Set: owner '" + ownerId + "' offered a non-finite value for '" + field + "' - ignored."); return false; } try { object[] array = Root(); lock (array[0]) { Dictionary<object, object> dictionary = (Dictionary<object, object>)array[1]; object value2; Dictionary<string, object> dictionary2 = (Dictionary<string, object>)(dictionary.TryGetValue(target, out value2) ? ((Dictionary<string, object>)value2) : (dictionary[target] = new Dictionary<string, object>())); object[] array2; if (dictionary2.TryGetValue(field, out value2)) { array2 = (object[])value2; } else { array2 = (object[])(dictionary2[field] = new object[4] { getter(), getter, setter, new List<object[]>() }); Info("captured base " + ((float)array2[0]).ToString("0.####") + " for " + Describe(target, field)); } List<object[]> list = (List<object[]>)array2[3]; object[] array3 = null; for (int i = 0; i < list.Count; i++) { if ((string)list[i][0] == ownerId) { array3 = list[i]; break; } } if (array3 == null) { array3 = new object[4] { ownerId, null, null, null }; list.Add(array3); } array3[1] = alive; array3[2] = (int)kind; array3[3] = value; Recompute(dictionary2, field, array2); SweepDeadOwners(dictionary, target, field); } return true; } catch (Exception ex) { Error("StatContrib.Set('" + field + "', '" + ownerId + "') threw: " + ex); return false; } } internal static void Remove(object target, string field, string ownerId) { if (target == null || string.IsNullOrEmpty(field) || string.IsNullOrEmpty(ownerId)) { return; } try { object[] array = Root(); lock (array[0]) { Dictionary<object, object> dictionary = (Dictionary<object, object>)array[1]; if (!dictionary.TryGetValue(target, out var value)) { return; } Dictionary<string, object> dictionary2 = (Dictionary<string, object>)value; if (!dictionary2.TryGetValue(field, out value)) { return; } object[] array2 = (object[])value; List<object[]> list = (List<object[]>)array2[3]; bool flag = false; for (int num = list.Count - 1; num >= 0; num--) { if ((string)list[num][0] == ownerId) { list.RemoveAt(num); flag = true; } } if (flag) { Recompute(dictionary2, field, array2); if (!dictionary2.ContainsKey(field) && dictionary2.Count == 0) { dictionary.Remove(target); } SweepDeadOwners(dictionary, target, field); } } } catch (Exception ex) { Error("StatContrib.Remove('" + field + "', '" + ownerId + "') threw: " + ex); } } internal static string DescribeField(object target, string field) { try { object[] array = Root(); lock (array[0]) { Dictionary<object, object> dictionary = (Dictionary<object, object>)array[1]; if (target == null || !dictionary.TryGetValue(target, out var value)) { return null; } if (!((Dictionary<string, object>)value).TryGetValue(field, out value)) { return null; } object[] array2 = (object[])value; List<object[]> list = (List<object[]>)array2[3]; string text = field + ": base " + ((float)array2[0]).ToString("0.####"); for (int i = 0; i < list.Count; i++) { text = text + ", " + (string)list[i][0] + " " + ((Kind)(int)list[i][2]/*cast due to .constrained prefix*/).ToString() + " " + ((float)list[i][3]).ToString("0.####"); } return text; } } catch { return null; } } private static void Recompute(Dictionary<string, object> byField, string field, object[] entry) { List<object[]> list = (List<object[]>)entry[3]; for (int num = list.Count - 1; num >= 0; num--) { Func<bool> func = list[num][1] as Func<bool>; bool flag = true; if (func != null) { try { flag = func(); } catch { flag = false; } } if (!flag) { Info("dropping contribution '" + (string)list[num][0] + "' to '" + field + "' - its owner is gone."); list.RemoveAt(num); } } float num2 = (float)entry[0]; Action<float> setter = (Action<float>)entry[2]; if (list.Count == 0) { byField.Remove(field); TryWrite(setter, num2, field, "base restore"); return; } float num3 = num2; float num4 = 1f; object[] array = null; for (int i = 0; i < list.Count; i++) { float num5 = (float)list[i][3]; switch ((Kind)(int)list[i][2]) { case Kind.Additive: num3 += num5; break; case Kind.Multiplicative: num4 *= num5; break; case Kind.Override: array = list[i]; break; } } float num6; if (array != null) { num6 = (float)array[3]; if (list.Count > 1) { Info("override '" + (string)array[0] + "' wins '" + field + "' over " + (list.Count - 1) + " other contribution(s)."); } } else { num6 = num3 * num4; } if (float.IsNaN(num6) || float.IsInfinity(num6)) { Error("composed value for '" + field + "' is non-finite; writing the base back instead. " + DescribeEntry(field, entry)); num6 = num2; } TryWrite(setter, num6, field, "recompute"); } private static void SweepDeadOwners(Dictionary<object, object> entries, object exceptTarget, string exceptField) { List<object> list = null; foreach (KeyValuePair<object, object> entry in entries) { Dictionary<string, object> dictionary = (Dictionary<string, object>)entry.Value; List<string> list2 = null; foreach (KeyValuePair<string, object> item in dictionary) { if (entry.Key == exceptTarget && item.Key == exceptField) { continue; } List<object[]> list3 = (List<object[]>)((object[])item.Value)[3]; for (int i = 0; i < list3.Count; i++) { Func<bool> func = list3[i][1] as Func<bool>; bool flag = true; if (func != null) { try { flag = func(); } catch { flag = false; } } if (!flag) { List<string> obj2 = list2 ?? new List<string>(); list2 = obj2; obj2.Add(item.Key); break; } } } if (list2 != null) { for (int j = 0; j < list2.Count; j++) { Recompute(dictionary, list2[j], (object[])dictionary[list2[j]]); } } if (dictionary.Count == 0) { List<object> obj3 = list ?? new List<object>(); list = obj3; obj3.Add(entry.Key); } } if (list != null) { for (int k = 0; k < list.Count; k++) { entries.Remove(list[k]); } } } private static void TryWrite(Action<float> setter, float value, string field, string why) { try { setter(value); } catch (Exception ex) { Info("write to '" + field + "' (" + why + ") failed: " + ex.Message); } } private static string Describe(object target, string field) { string text; try { text = target.GetType().Name; } catch { text = "<target>"; } return text + "." + field; } private static string DescribeEntry(string field, object[] entry) { List<object[]> list = (List<object[]>)entry[3]; string text = "base " + ((float)entry[0]).ToString("0.####") + ";"; for (int i = 0; i < list.Count; i++) { text = text + " " + (string)list[i][0] + "=" + ((Kind)(int)list[i][2]/*cast due to .constrained prefix*/).ToString() + ":" + ((float)list[i][3]).ToString("0.####"); } return text; } private static void Info(string m) { if (Log != null) { Log.LogInfo((object)("StatContrib: " + m)); } } private static void Error(string m) { if (Log != null) { Log.LogError((object)("StatContrib: " + m)); } } } internal interface ITuningKnob { string Name { get; } string Help { get; } bool IsInert { get; } void WriteJson(JObject o); void ReadJson(JObject o); string Describe(); void DrawMenuRow(); string BakeSnippet(); } internal sealed class TuningFile { private readonly string _guid; private readonly string _tag; private readonly string _readme; private readonly ManualLogSource _log; private readonly List<ITuningKnob> _order = new List<ITuningKnob>(); private readonly Dictionary<string, ITuningKnob> _byName = new Dictionary<string, ITuningKnob>(StringComparer.OrdinalIgnoreCase); private readonly List<Action> _reloadHandlers = new List<Action>(); private DateTime _lastWrite = DateTime.MinValue; private JObject _baseline; private JObject _previousBaseline; internal string LastBakePath = "<bake not run>"; private string _menuSignature; internal string Status { get; private set; } = "not loaded"; internal string Path { get; private set; } internal string BaselinePath { get; private set; } internal IList<ITuningKnob> Knobs => _order; internal bool AllInert { get { for (int i = 0; i < _order.Count; i++) { if (!_order[i].IsInert) { return false; } } return true; } } internal TuningFile(string pluginGuid, string logTag, string readme, ManualLogSource log) { _guid = pluginGuid; _tag = logTag; _readme = readme; _log = log; } internal T Add<T>(T knob) where T : ITuningKnob { if (knob == null) { return default(T); } if (_byName.ContainsKey(knob.Name)) { Warn("duplicate tuning knob '" + knob.Name + "' ignored. Two declarations of one name means one of them is silently dead."); return knob; } _byName[knob.Name] = knob; _order.Add(knob); return knob; } internal ITuningKnob Find(string name) { if (!_byName.TryGetValue(name, out var value)) { return null; } return value; } internal void OnReload(Action handler) { if (handler != null && !_reloadHandlers.Contains(handler)) { _reloadHandlers.Add(handler); } } internal void Initialise(string configDir) { try { Path = System.IO.Path.Combine(configDir, _guid + ".tuning.json"); BaselinePath = System.IO.Path.Combine(configDir, _guid + ".tuning.baseline.json"); _baseline = BuildKnobObject(); _previousBaseline = ReadPreviousBaseline(); WriteBaseline(); if (!File.Exists(Path)) { Save("created at the declared defaults"); } else { Load(); } } catch (Exception e) { Status = "FAILED to initialise: " + Brief(e); Error(Status); } } internal bool PollForChanges() { if (string.IsNullOrEmpty(Path)) { return false; } try { if (!File.Exists(Path)) { return false; } if (File.GetLastWriteTimeUtc(Path) <= _lastWrite) { return false; } Load(); Info("tuning reloaded - " + Status); FireReloadHandlers(); return true; } catch (Exception e) { Warn("tuning poll threw: " + Brief(e)); return false; } } private void FireReloadHandlers() { for (int i = 0; i < _reloadHandlers.Count; i++) { try { _reloadHandlers[i](); } catch (Exception ex) { Error("a tuning re-apply handler threw: " + ex); } } } private void Load() { string text; try { text = File.ReadAllText(Path); } catch (Exception e) { Status = "could not read the file (" + Brief(e) + "); previous values stand"; Error(Status); return; } _lastWrite = File.GetLastWriteTimeUtc(Path); JObject val; try { val = JObject.Parse(text); } catch (Exception e2) { Status = "MALFORMED tuning file (" + Brief(e2) + "); previous values stand"; Error(Status + ". Fix " + Path + " - nothing was applied from it."); return; } JToken obj = val["knobs"]; JObject val2 = (JObject)(object)((obj is JObject) ? obj : null); if (val2 == null) { Status = "tuning file has no 'knobs' object; every knob left at its declared default"; Warn(Status); return; } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; foreach (KeyValuePair<string, JToken> item in val2) { if (!_byName.TryGetValue(item.Key, out var value)) { num2++; Warn("tuning file names unknown knob '" + item.Key + "' - ignored. This is a typo or a rename, not a tuning value."); continue; } JToken value2 = item.Value; JObject val3 = (JObject)(object)((value2 is JObject) ? value2 : null); if (val3 == null) { continue; } if (FollowsNewDeclaration(item.Key, val3)) { num4++; num++; continue; } try { value.ReadJson(val3); } catch (Exception e3) { Warn("knob '" + item.Key + "' could not be read: " + Brief(e3)); } num++; if (!value.IsInert) { num3++; } } Status = num + " knob(s) read, " + num3 + " active" + ((num2 > 0) ? (", " + num2 + " unknown name(s) ignored") : "") + ((num4 > 0) ? (", " + num4 + " untouched knob(s) moved to this build's new declared value") : ""); if (num3 == 0) { Status += " - every knob inert"; } int count = DriftReport().Count; if (count > 0) { Status = Status + "; " + count + " differ(s) from source"; } if (num2 > 0 || num < _order.Count || num4 > 0) { string status = Status; Save("rewritten to match this version's knobs (" + (_order.Count - num) + " added, " + num2 + " stale name(s) dropped)"); Status = status + "; file rewritten to match this version's knobs"; } } private JObject ReadPreviousBaseline() { try { if (!File.Exists(BaselinePath)) { return null; } JToken obj = JObject.Parse(File.ReadAllText(BaselinePath))["knobs"]; return (JObject)(object)((obj is JObject) ? obj : null); } catch { return null; } } private bool FollowsNewDeclaration(string name, JObject fromFile) { if (_previousBaseline == null || _baseline == null) { return false; } JToken obj = _previousBaseline[name]; JObject val = (JObject)(object)((obj is JObject) ? obj : null); JToken obj2 = _baseline[name]; JObject val2 = (JObject)(object)((obj2 is JObject) ? obj2 : null); if (val == null || val2 == null) { return false; } if (JToken.DeepEquals((JToken)(object)val, (JToken)(object)val2)) { return false; } return JToken.DeepEquals((JToken)(object)val, (JToken)(object)fromFile); } internal void Save(string why) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown try { JObject val = new JObject(); val["_readme"] = JToken.op_Implicit(_readme + " The '_' field in each knob is a comment and is ignored. Saved changes are picked up within a second - no restart. This file is NOT shipped: values dialled in here reach nobody else until they are written back into the mod's source declarations."); val["knobs"] = (JToken)(object)BuildKnobObject(); EnsureDir(Path); File.WriteAllText(Path, ((JToken)val).ToString((Formatting)1, Array.Empty<JsonConverter>())); _lastWrite = File.GetLastWriteTimeUtc(Path); Status = why; Info("tuning file " + why + " -> " + Path); } catch (Exception e) { Status = "could not write the tuning file: " + Brief(e); Error(Status); } } private JObject BuildKnobObject() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown JObject val = new JObject(); for (int i = 0; i < _order.Count; i++) { ITuningKnob tuningKnob = _order[i]; JObject val2 = new JObject(); val2["_"] = JToken.op_Implicit(tuningKnob.Help); tuningKnob.WriteJson(val2); val[tuningKnob.Name] = (JToken)(object)val2; } return val; } private void WriteBaseline() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown try { JObject val = new JObject(); val["_readme"] = JToken.op_Implicit("Generated every launch. These are the knob values as DECLARED IN SOURCE, which is what a build handed to anyone else runs at. Diff the sibling .tuning.json against this to see which dialled-in numbers would be lost by shipping. Do not edit; edit the source declarations and relaunch."); val["knobs"] = (JToken)(object)_baseline; EnsureDir(BaselinePath); File.WriteAllText(BaselinePath, ((JToken)val).ToString((Formatting)1, Array.Empty<JsonConverter>())); } catch (Exception e) { Warn("could not write the tuning baseline (" + Brief(e) + "). The release drift check has nothing to compare against for this mod."); } } internal List<string> DriftReport() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown List<string> list = new List<string>(); if (_baseline == null) { return list; } for (int i = 0; i < _order.Count; i++) { ITuningKnob tuningKnob = _order[i]; JObject val = new JObject(); tuningKnob.WriteJson(val); JToken obj = _baseline[tuningKnob.Name]; JObject val2 = (JObject)(object)((obj is JObject) ? obj : null); if (val2 == null) { continue; } foreach (KeyValuePair<string, JToken> item in val) { JToken val3 = val2[item.Key]; if (val3 != null && !JToken.DeepEquals(val3, item.Value)) { list.Add(tuningKnob.Name + "." + item.Key + ": source " + ((object)val3)?.ToString() + " -> live " + (object)item.Value); } } } return list; } internal string WriteBakeSnippet(string outputDir) { try { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("// " + _tag + " - tuning bake, taken from the live tuning file."); stringBuilder.AppendLine("//"); stringBuilder.AppendLine("// Paste over the knob declarations. After this the declared defaults"); stringBuilder.AppendLine("// and the tuning file agree, the drift check is clean, and a build"); stringBuilder.AppendLine("// handed to anyone else plays at the numbers you dialled in."); stringBuilder.AppendLine("//"); stringBuilder.AppendLine("// Source : " + (Path ?? "<none>")); stringBuilder.AppendLine("// State : " + Status); stringBuilder.AppendLine(); for (int i = 0; i < _order.Count; i++) { stringBuilder.AppendLine(_order[i].BakeSnippet()); } List<string> list = DriftReport(); stringBuilder.AppendLine(); if (list.Count == 0) { stringBuilder.AppendLine("// Nothing has drifted - source already matches the tuning file."); } else { stringBuilder.AppendLine("// Differs from source (" + list.Count + "):"); for (int j = 0; j < list.Count; j++) { stringBuilder.AppendLine("// " + list[j]); } } string text = System.IO.Path.Combine(outputDir, _guid + ".tuning.bake.cs.txt"); EnsureDir(text); File.WriteAllText(text, stringBuilder.ToString()); Info("tuning bake snippet -> " + text); return text; } catch (Exception ex) { Error("tuning bake snippet threw: " + ex); return "<failed: " + Brief(ex) + ">"; } } internal void DrawMenu() { try { GUILayout.Label("Tuning : " + Status, Array.Empty<GUILayoutOption>()); GUILayout.Label(" " + (Path ?? "<not initialised>"), Array.Empty<GUILayoutOption>()); for (int i = 0; i < _order.Count; i++) { _order[i].DrawMenuRow(); } string text = MenuSignature(); if (_menuSignature == null) { _menuSignature = text; } else if (text != _menuSignature) { _menuSignature = text; FireReloadHandlers(); } List<string> list = DriftReport(); GUILayout.Space(6f); if (list.Count == 0) { GUILayout.Label("Source and tuning file agree - safe to package.", Array.Empty<GUILayoutOption>()); } else { GUILayout.Label("NOT IN SOURCE (" + list.Count + ") - a build sent to anyone else", Array.Empty<GUILayoutOption>()); GUILayout.Label("would run at the source values, not these:", Array.Empty<GUILayoutOption>()); for (int j = 0; j < list.Count; j++) { GUILayout.Label(" " + list[j], Array.Empty<GUILayoutOption>()); } } GUILayout.Space(4f); if (GUILayout.Button("Save these values to the tuning file", Array.Empty<GUILayoutOption>())) { Save("saved from the F5 menu"); } if (GUILayout.Button("Reload the tuning file now", Array.Empty<GUILayoutOption>())) { _lastWrite = DateTime.MinValue; PollForChanges(); } if (GUILayout.Button("Write the bake snippet (the release step)", Array.Empty<GUILayoutOption>())) { LastBakePath = WriteBakeSnippet(Paths.ConfigPath); } GUILayout.Label(" " + LastBakePath, Array.Empty<GUILayoutOption>()); } catch (Exception e) { GUILayout.Label("Tuning menu failed to draw: " + Brief(e), Array.Empty<GUILayoutOption>()); } } private string MenuSignature() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < _order.Count; i++) { JObject val = new JObject(); _order[i].WriteJson(val); stringBuilder.Append(((JToken)val).ToString((Formatting)0, Array.Empty<JsonConverter>())); } return stringBuilder.ToString(); } private static void EnsureDir(string file) { string directoryName = System.IO.Path.GetDirectoryName(file); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } } private static string Brief(Exception e) { if (e != null) { return e.GetType().Name + ": " + e.Message; } return "<null>"; } private void Info(string m) { if (_log != null) { _log.LogInfo((object)(_tag + ": " + m)); } } private void Warn(string m) { if (_log != null) { _log.LogWarning((object)(_tag + ": " + m)); } } private void Error(string m) { if (_log != null) { _log.LogError((object)(_tag + ": " + m)); } } internal static string F(float v) { return v.ToString("0.####", CultureInfo.InvariantCulture); } } internal sealed class TuningScalar : ITuningKnob { public float Value; public readonly float Declared; public readonly float Min; public readonly float Max; public readonly float Sentinel; private readonly NumericField _field = new NumericField(); public string Name { get; private set; } public string Help { get; private set; } public bool IsInert { get { if (!float.IsNaN(Sentinel) && Nearly(Value, Sentinel)) { return true; } return Nearly(Value, Declared); } } public bool IsSentinel { get { if (!float.IsNaN(Sentinel)) { return Nearly(Value, Sentinel); } return false; } } internal TuningScalar(string name, string help, float declared, float min, float max, float sentinel = float.NaN) { Name = name; Help = help; Value = declared; Declared = declared; Min = min; Max = max; Sentinel = sentinel; } public void WriteJson(JObject o) { o["value"] = JToken.op_Implicit(Value); } public void ReadJson(JObject o) { JToken val = o["value"]; if (val == null) { return; } try { float num = Extensions.Value<float>((IEnumerable<JToken>)val); if (!float.IsNaN(num) && !float.IsInfinity(num)) { Value = num; } } catch { } } public string Describe() { if (!IsSentinel) { return TuningFile.F(Value); } return "keep the game's own value"; } public void DrawMenuRow() { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(Name + " " + Describe(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(280f) }); Value = _field.Draw(Value); Value = GUILayout.HorizontalSlider(Value, Min, Max, Array.Empty<GUILayoutOption>()); GUILayout.EndHorizontal(); } public string BakeSnippet() { string text = (float.IsNaN(Sentinel) ? "" : (", " + TuningFile.F(Sentinel) + "f")); return "Scalar(\"" + Name + "\", \"" + Help.Replace("\"", "\\\"") + "\", " + TuningFile.F(Value) + "f, " + TuningFile.F(Min) + "f, " + TuningFile.F(Max) + "f" + text + ");"; } private static bool Nearly(float a, float b) { return Math.Abs(a - b) < 0.0001f; } } internal sealed class TuningString : ITuningKnob { public string Value; public readonly string Declared; public string Name { get; private set; } public string Help { get; private set; } public bool HasValue => !string.IsNullOrEmpty(Value); public bool IsInert => string.Equals(Value, Declared, StringComparison.Ordinal); internal TuningString(string name, string help, string declared) { Name = name; Help = help; Value = declared ?? ""; Declared = declared ?? ""; } public void WriteJson(JObject o) { o["text"] = JToken.op_Implicit(Value ?? ""); } public void ReadJson(JObject o) { JToken val = o["text"]; if (val == null) { return; } try { string text = Extensions.Value<string>((IEnumerable<JToken>)val); Value = (text ?? "").Trim(); } catch { } } public string Describe() { if (!HasValue) { return "the game's own choice"; } return Value; } public void DrawMenuRow() { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(Name, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(280f) }); Value = GUILayout.TextField(Value ?? "", Array.Empty<GUILayoutOption>()) ?? ""; GUILayout.EndHorizontal(); } public string BakeSnippet() { return "Text(\"" + Name + "\", \"" + Help.Replace("\"", "\\\"") + "\", \"" + (Value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\");"; } } internal sealed class NumericField { private string _text; private float _synced = float.NaN; internal float Draw(float value, float width = 64f, bool integer = false) { if (_text == null || value != _synced) { _text = (integer ? ((int)Math.Round(value)).ToString(CultureInfo.InvariantCulture) : TuningFile.F(value)); _synced = value; } string text = GUILayout.TextField(_text, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(width) }); if (text == _text) { return value; } _text = text; if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return value; } if (float.IsNaN(result) || float.IsInfinity(result)) { return value; } if (integer) { result = (float)Math.Round(result); } _synced = result; return result; } } internal sealed class TuningToggle : ITuningKnob { public bool Value; public readonly bool Declared; public string Name { get; private set; } public string Help { get; private set; } public bool IsInert => Value == Declared; internal TuningToggle(string name, string help, bool declared) { Name = name; Help = help; Value = declared; Declared = declared; } public void WriteJson(JObject o) { o["on"] = JToken.op_Implicit(Value); } public void ReadJson(JObject o) { JToken val = o["on"]; if (val == null) { return; } try { Value = Extensions.Value<bool>((IEnumerable<JToken>)val); } catch { } } public string Describe() { if (!Value) { return "off"; } return "on"; } public void DrawMenuRow() { Value = GUILayout.Toggle(Value, " " + Name + " - " + Help, Array.Empty<GUILayoutOption>()); } public string BakeSnippet() { return "Toggle(\"" + Name + "\", \"" + Help.Replace("\"", "\\\"") + "\", " + (Value ? "true" : "false") + ");"; } } namespace VoidCrewQuickstart { internal static class MenuInjection { [HarmonyPatch(typeof(MainMenu), "StartAsTitleMenu")] internal static class Patch_MainMenu_StartAsTitleMenu { [HarmonyPostfix] private static void Postfix(MainMenu __instance) { try { Inject(Traverse.Create((object)__instance).Field("_mainmenuRoot").GetValue<VisualElement>()); } catch (Exception ex) { Plugin.Log.LogError((object)("Quickstart: injection at StartAsTitleMenu failed: " + ex)); } } } [HarmonyPatch(typeof(TitleMenu), "OnEnter")] internal static class Patch_TitleMenu_OnEnter { [HarmonyPostfix] private static void Postfix(TitleMenu __instance) { try { UIDocument value = Traverse.Create((object)__instance).Field("UIDoc").GetValue<UIDocument>(); if (!((Object)(object)value == (Object)null)) { Inject(value.rootVisualElement); } } catch (Exception ex) { Plugin.Log.LogError((object)("Quickstart: injection at TitleMenu.OnEnter failed: " + ex)); } } } private const string PICKING_IGNORE = "picking-mode-setter-ignore"; private static bool _loggedInject; private static MethodInfo[] _gameReadyChecks; private static bool _loggedReadyFailure; public static void Inject(VisualElement root) { if (root == null || !QuickstartConfig.Enabled.Value) { return; } Button val = UQueryExtensions.Q<Button>(root, "button_quickstart", (string)null); if (val != null) { MenuLayout.Apply(root, val); return; } Button val2 = UQueryExtensions.Q<Button>(root, "button_solo", (string)null); if (val2 == null) { Plugin.Log.LogWarning((object)"Quickstart: 'button_solo' not found in the title menu - the menu UXML changed. No button injected."); return; } VisualElement parent = ((VisualElement)val2).parent; if (parent == null) { Plugin.Log.LogWarning((object)"Quickstart: solo button has no parent; not injecting."); return; } Button btn = BuildButton(val2, QuickstartConfig.ButtonLabel.Value); int num = parent.IndexOf((VisualElement)(object)val2); if (!QuickstartConfig.PlaceAboveSoloPlay.Value) { num++; } parent.Insert(num, (VisualElement)(object)btn); try { UIAudioProvider.AddSounds((VisualElement)(object)btn, "button"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Quickstart: could not attach menu sounds: " + ex.Message)); } UItoolkitExtensionMethods.AddClickedEvent(btn, (Action)OnClicked); ((VisualElement)btn).schedule.Execute((Action)delegate { UpdateAvailability(btn); }).Every(250L); UpdateAvailability(btn); MenuLayout.Apply(root, btn); if (!_loggedInject) { _loggedInject = true; Plugin.Log.LogInfo((object)("Quickstart: button injected " + (QuickstartConfig.PlaceAboveSoloPlay.Value ? "above" : "below") + " Solo Play.")); } } private static Button BuildButton(Button template, string label) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown Button val = new Button(); ((VisualElement)val).name = "button_quickstart"; foreach (string @class in ((VisualElement)template).GetClasses()) { ((VisualElement)val).AddToClassList(@class); } ((VisualElement)val).AddToClassList("picking-mode-setter-ignore"); ((VisualElement)val).pickingMode = (PickingMode)0; Label val2 = UQueryExtensions.Q<Label>((VisualElement)(object)template, (string)null, (string)null); if (val2 != null) { Label val3 = new Label(label); ((VisualElement)val3).name = ((VisualElement)val2).name; foreach (string class2 in ((VisualElement)val2).GetClasses()) { ((VisualElement)val3).AddToClassList(class2); } ((VisualElement)val3).AddToClassList("picking-mode-setter-ignore"); ((VisualElement)val3).pickingMode = (PickingMode)1; ((VisualElement)val).Add((VisualElement)(object)val3); } else { ((TextElement)val).text = label; } return val; } private static void UpdateAvailability(Button btn) { try { bool enabled = (((Focusable)btn).focusable = IsReadyToLaunch() && !QuickstartLauncher.InFlight); ((VisualElement)btn).SetEnabled(enabled); } catch { } } internal static bool IsReadyToLaunch() { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Invalid comparison between Unknown and I4 try { if (_gameReadyChecks == null) { ResolveReadyChecks(); } if (_gameReadyChecks != null && _gameReadyChecks.Length == 3) { bool num = (bool)_gameReadyChecks[0].Invoke(null, null); bool flag = (bool)_gameReadyChecks[1].Invoke(null, null); bool flag2 = (bool)_gameReadyChecks[2].Invoke(null, null); return num && flag && !flag2 && VoiceReadiness.IsReady(); } return (PhotonNetwork.OfflineMode || PhotonNetwork.InLobby || (int)PhotonNetwork.NetworkClientState == 4) && VoiceReadiness.IsReady(); } catch (Exception ex) { if (!_loggedReadyFailure) { _loggedReadyFailure = true; Plugin.Log.LogWarning((object)("Quickstart: readiness check failed, treating the button as available: " + ex.Message)); } return true; } } private static void ResolveReadyChecks() { MethodInfo methodInfo = AccessTools.Method(typeof(MainMenu), "IsConnectedToLobby", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(MainMenu), "IsConnectedToUnityCloud", (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(MainMenu), "IsLocalProfileBeingReset", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null || methodInfo3 == null) { Plugin.Log.LogWarning((object)"Quickstart: MainMenu's readiness helpers moved; falling back to a Photon-only check."); _gameReadyChecks = new MethodInfo[0]; return; } _gameReadyChecks = new MethodInfo[3] { methodInfo, methodInfo2, methodInfo3 }; } private static void OnClicked() { try { QuickstartLauncher.Launch(); } catch (Exception ex) { Plugin.Log.LogError((object)("Quickstart: launch threw: " + ex)); QuickstartLauncher.Status = "failed: " + ex.Message; } } } internal static class MenuLayout { private sealed class ButtonMetrics { public bool Captured; public float BaseMarginTop; public float BaseMarginBottom; } internal static readonly bool APPLY_SHIFT = true; internal static readonly float STACK_RECENTRE_FACTOR = 0.5f; internal static readonly float EXTRA_SHIFT_UP_PX = 0f; internal static readonly float INJECTED_BUTTON_MARGIN_SCALE = 1f; internal static readonly string SHIFT_TARGET_NAME = ""; internal static string Status = "not applied yet"; internal static float MeasuredSlotPx = -1f; internal static float AppliedShiftPx; private static bool _loggedApply; private static IVisualElementScheduledItem _shiftItem; internal const string LogbookFinding = "The Logbook cannot be opened from the title menu. MainMenu.OnLogbookButtonClicked is wired only in StartAsEscapeMenu (button_logbook), and its body is ClientGame.Current.ModelEventBus.OnLocalToggleLogbookRequest.Publish() plus Singleton<ToggleInGameMenu>.Instance.CloseFromLogBook(). ClientGame.Current is assigned in ClientGame.Awake, which is a session-scene object, so it is null in the title menu. The receiving end is worse: UI.Codex.LogBookManager subscribes to that channel in Start via the same unguarded ClientGame.Current, and its Toggle() and SetTerminalState() dereference LocalPlayer.I - it exists only inside a run. So the prescribed fallback applies: hide the Tutorial button and leave no gap."; public static void Apply(VisualElement root, Button injected) { bool tutorialHidden = ApplyTutorialHide(root); ApplyShift(root, injected, tutorialHidden); } private static bool ApplyTutorialHide(VisualElement root) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) Button val = null; try { val = UQueryExtensions.Q<Button>(root, "button_awakening", (string)null); } catch { } if (val == null) { Status = "'button_awakening' not found - Tutorial button left alone"; return false; } bool value = QuickstartConfig.HideTutorial.Value; try { if (value) { ((VisualElement)val).style.display = StyleEnum<DisplayStyle>.op_Implicit((DisplayStyle)1); ((VisualElement)val).SetEnabled(false); ((Focusable)val).focusable = false; } else { ((VisualElement)val).style.display = StyleEnum<DisplayStyle>.op_Implicit((StyleKeyword)1); ((VisualElement)val).SetEnabled(true); } } catch (Exception ex) { Status = "could not set the Tutorial button's display: " + ex.Message; Plugin.Log.LogWarning((object)("Quickstart: " + Status)); return false; } Status = "Tutorial button " + (value ? "hidden" : "shown") + " (Button/HideTutorial)"; return value; } private static void ApplyShift(VisualElement root, Button injected, bool tutorialHidden) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if (injected == null) { return; } VisualElement val = ResolveShiftTarget(root, injected); if (val == null) { return; } try { val.style.translate = StyleTranslate.op_Implicit(new Translate(Length.op_Implicit(0f), Length.op_Implicit(0f))); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Quickstart: could not reset the menu translate: " + ex.Message)); return; } AppliedShiftPx = 0f; if (APPLY_SHIFT) { int num = 1 - (tutorialHidden ? 1 : 0); if (num == 0 && EXTRA_SHIFT_UP_PX == 0f) { MeasuredSlotPx = -1f; } else { ScheduleMeasureAndShift(val, injected, num); } } } private static VisualElement ResolveShiftTarget(VisualElement root, Button injected) { if (!string.IsNullOrEmpty(SHIFT_TARGET_NAME)) { VisualElement val = null; try { val = UQueryExtensions.Q<VisualElement>(root, SHIFT_TARGET_NAME, (string)null); } catch { } if (val != null) { return val; } Plugin.Log.LogWarning((object)("Quickstart: SHIFT_TARGET_NAME '" + SHIFT_TARGET_NAME + "' is not in the title menu; using the button's parent.")); } return ((VisualElement)injected).parent; } private static void ScheduleMeasureAndShift(VisualElement stack, Button injected, int netAdded) { Pause(_shiftItem); int[] attempts = new int[1]; IVisualElementScheduledItem[] item = (IVisualElementScheduledItem[])(object)new IVisualElementScheduledItem[1]; item[0] = ((VisualElement)injected).schedule.Execute((Action)delegate { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) try { attempts[0]++; Rect layout = ((VisualElement)injected).layout; float height = ((Rect)(ref layout)).height; if (float.IsNaN(height) || height <= 0f) { if (attempts[0] >= 40) { Pause(item[0]); Plugin.Log.LogWarning((object)"Quickstart: the injected button never resolved a height; menu left unshifted."); } } else { ButtonMetrics buttonMetrics = CaptureMetrics(injected); ApplyMarginScale(injected, buttonMetrics); float num = (MeasuredSlotPx = height + buttonMetrics.BaseMarginTop * INJECTED_BUTTON_MARGIN_SCALE + buttonMetrics.BaseMarginBottom * INJECTED_BUTTON_MARGIN_SCALE); float num2 = (AppliedShiftPx = STACK_RECENTRE_FACTOR * num * (float)netAdded + EXTRA_SHIFT_UP_PX); stack.style.translate = StyleTranslate.op_Implicit(new Translate(Length.op_Implicit(0f), Length.op_Implicit(0f - num2))); Pause(item[0]); if (!_loggedApply) { _loggedApply = true; Plugin.Log.LogDebug((object)("Quickstart: menu shifted up " + num2.ToString("0.##") + " reference px (slot " + num.ToString("0.##") + " px, net " + netAdded + " button(s) added). " + Status)); } } } catch (Exception ex) { Pause(item[0]); Plugin.Log.LogWarning((object)("Quickstart: layout pass threw, menu left unshifted: " + ex)); } }).Every(100L); _shiftItem = item[0]; } private static void Pause(IVisualElementScheduledItem item) { try { if (item != null) { item.Pause(); } } catch { } } private static ButtonMetrics CaptureMetrics(Button injected) { ButtonMetrics buttonMetrics = ((VisualElement)injected).userData as ButtonMetrics; if (buttonMetrics == null) { buttonMetrics = (ButtonMetrics)(((VisualElement)injected).userData = new ButtonMetrics()); } if (!buttonMetrics.Captured) { buttonMetrics.BaseMarginTop = Sane(((VisualElement)injected).resolvedStyle.marginTop); buttonMetrics.BaseMarginBottom = Sane(((VisualElement)injected).resolvedStyle.marginBottom); buttonMetrics.Captured = true; } return buttonMetrics; } private static void ApplyMarginScale(Button injected, ButtonMetrics metrics) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) if (INJECTED_BUTTON_MARGIN_SCALE != 1f) { ((VisualElement)injected).style.marginTop = StyleLength.op_Implicit(metrics.BaseMarginTop * INJECTED_BUTTON_MARGIN_SCALE); ((VisualElement)injected).style.marginBottom = StyleLength.op_Implicit(metrics.BaseMarginBottom * INJECTED_BUTTON_MARGIN_SCALE); } } private static float Sane(float v) { if (!float.IsNaN(v) && !float.IsInfinity(v)) { return v; } return 0f; } } internal static class MenuTreeDump { private const int MAX_DEPTH = 40; internal static string LastResult = ""; public static string Write() { if (!TryGetRoot(out var root, out var source)) { throw new InvalidOperationException("the title menu is not up (TitleMenu.Instance / UIDoc is null). Open the main menu, then press this."); } StringBuilder stringBuilder = new StringBuilder(); WriteHeader(stringBuilder, root, source); stringBuilder.AppendLine("visual tree"); stringBuilder.AppendLine(new string('-', 100)); int num = Walk(stringBuilder, root, 0); stringBuilder.AppendLine(); stringBuilder.AppendLine(num + " elements."); stringBuilder.AppendLine(); WriteButtonSummary(stringBuilder, root); string text = Path.Combine(Paths.BepInExRootPath, "Quickstart"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, "menu-tree.txt"); File.WriteAllText(text2, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); return text2; } private static bool TryGetRoot(out VisualElement root, out string source) { root = null; source = null; try { TitleMenu instance = TitleMenu.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.UIDoc == (Object)null) { return false; } root = instance.UIDoc.rootVisualElement; source = "TitleMenu.Instance.UIDoc.rootVisualElement"; return root != null; } catch { return false; } } private static void WriteHeader(StringBuilder sb, VisualElement root, string source) { sb.AppendLine("Quickstart main-menu visual tree dump"); sb.AppendLine("v1.0.1 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); sb.AppendLine(new string('=', 100)); sb.AppendLine("root : " + source); sb.AppendLine("screen : " + Screen.width + "x" + Screen.height + " aspect " + Ratio(Screen.width, Screen.height) + " panel scale (Screen.height/1080) = " + F((float)Screen.height / 1080f)); sb.AppendLine("reference : root resolvedStyle " + F(root.resolvedStyle.width) + " x " + F(root.resolvedStyle.height) + " px"); sb.AppendLine(" (layout is computed in reference px; the usable reference"); sb.AppendLine(" WIDTH moves with aspect ratio, the height is always 1080)"); sb.AppendLine("tutorial : " + (QuickstartConfig.HideTutorial.Value ? "hidden" : "shown") + " (Button/HideTutorial)"); sb.AppendLine("layout : " + MenuLayout.Status); string[] obj = new string[8] { " APPLY_SHIFT=", null, null, null, null, null, null, null }; bool aPPLY_SHIFT = MenuLayout.APPLY_SHIFT; obj[1] = aPPLY_SHIFT.ToString(); obj[2] = " STACK_RECENTRE_FACTOR="; obj[3] = F(MenuLayout.STACK_RECENTRE_FACTOR); obj[4] = " EXTRA_SHIFT_UP_PX="; obj[5] = F(MenuLayout.EXTRA_SHIFT_UP_PX); obj[6] = " INJECTED_BUTTON_MARGIN_SCALE="; obj[7] = F(MenuLayout.INJECTED_BUTTON_MARGIN_SCALE); sb.AppendLine(string.Concat(obj)); sb.AppendLine(" measured slot = " + F(MenuLayout.MeasuredSlotPx) + " px, applied shift = " + F(MenuLayout.AppliedShiftPx) + " px"); sb.AppendLine(); sb.AppendLine("per element: #name <Type> [classes] pick= disp= vis= focus= enabled="); sb.AppendLine(" world=(x,y,w,h) layout=(x,y,w,h) pos= translate="); sb.AppendLine(" margin=(t,r,b,l) padding=(t,r,b,l) inset=(t,r,b,l)"); sb.AppendLine(" flex=(dir,grow,shrink) size=(w,h)"); sb.AppendLine(); } private static int Walk(StringBuilder sb, VisualElement ve, int depth) { if (ve == null) { return 0; } string value = new string(' ', depth * 2); sb.Append(value).AppendLine(Headline(ve)); sb.Append(value).Append(" ").AppendLine(Box(ve)); sb.Append(value).Append(" ").AppendLine(Spacing(ve)); sb.Append(value).Append(" ").AppendLine(Flex(ve)); int num = 1; if (depth >= 40) { sb.Append(value).AppendLine(" ... depth limit reached, not descending"); return num; } for (int i = 0; i < ve.childCount; i++) { num += Walk(sb, ve[i], depth + 1); } return num; } private static string Headline(VisualElement ve) { //IL_00ac: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append('#').Append(string.IsNullOrEmpty(ve.name) ? "(unnamed)" : ve.name); stringBuilder.Append(" <").Append(((object)ve).GetType().Name).Append('>'); string text = Classes(ve); if (text.Length > 0) { stringBuilder.Append(" [").Append(text).Append(']'); } stringBuilder.Append(" pick=").Append(ve.pickingMode); stringBuilder.Append(" disp=").Append(Safe(() => ((object)ve.resolvedStyle.display/*cast due to .constrained prefix*/).ToString())); stringBuilder.Append(" vis=").Append(Safe(() => ((object)ve.resolvedStyle.visibility/*cast due to .constrained prefix*/).ToString())); stringBuilder.Append(" focus=").Append(((Focusable)ve).focusable); stringBuilder.Append(" enabled=").Append(ve.enabledInHierarchy); string value = TextOf(ve); if (!string.IsNullOrEmpty(value)) { stringBuilder.Append(" text=\"").Append(value).Append('"'); } return stringBuilder.ToString(); } private static string Box(VisualElement ve) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) Rect worldBound = ve.worldBound; Rect layout = ve.layout; return "world=(" + F(((Rect)(ref worldBound)).x) + "," + F(((Rect)(ref worldBound)).y) + "," + F(((Rect)(ref worldBound)).width) + "," + F(((Rect)(ref worldBound)).height) + ") layout=(" + F(((Rect)(ref layout)).x) + "," + F(((Rect)(ref layout)).y) + "," + F(((Rect)(ref layout)).width) + "," + F(((Rect)(ref layout)).height) + ") pos=" + Safe(() => ((object)ve.resolvedStyle.position/*cast due to .constrained prefix*/).ToString()) + " translate=" + Safe(() => ((object)ve.resolvedStyle.translate/*cast due to .constrained prefix*/).ToString()); } private static string Spacing(VisualElement ve) { IResolvedStyle resolvedStyle = ve.resolvedStyle; return "margin=(" + F(resolvedStyle.marginTop) + "," + F(resolvedStyle.marginRight) + "," + F(resolvedStyle.marginBottom) + "," + F(resolvedStyle.marginLeft) + ") padding=(" + F(resolvedStyle.paddingTop) + "," + F(resolvedStyle.paddingRight) + "," + F(resolvedStyle.paddingBottom) + "," + F(resolvedStyle.paddingLeft) + ") inset=(" + F(resolvedStyle.top) + "," + F(resolvedStyle.right) + "," + F(resolvedStyle.bottom) + "," + F(resolvedStyle.left) + ")"; } private static string Flex(VisualElement ve) { IResolvedStyle s = ve.resolvedStyle; return "flex=(" + Safe(() => ((object)s.flexDirection/*cast due to .constrained prefix*/).ToString()) + "," + F(s.flexGrow) + "," + F(s.flexShrink) + ") align=(" + Safe(() => ((object)s.alignItems/*cast due to .constrained prefix*/).ToString()) + "," + Safe(() => ((object)s.justifyContent/*cast due to .constrained prefix*/).ToString()) + ") size=(" + F(s.width) + "," + F(s.height) + ")"; } private static void WriteButtonSummary(StringBuilder sb, VisualElement root) { //IL_00f3: 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_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) sb.AppendLine("button summary (tree order)"); sb.AppendLine(new string('-', 100)); List<Button> list = new List<Button>(); Collect(root, list); if (list.Count == 0) { sb.AppendLine(" no Buttons found - the menu UXML changed."); return; } sb.AppendLine(" name parent y h mTop mBot disp"); for (int i = 0; i < list.Count; i++) { Button b = list[i]; StringBuilder stringBuilder = sb.Append(" ").Append(Pad(string.IsNullOrEmpty(((VisualElement)b).name) ? "(unnamed)" : ((VisualElement)b).name, 25)).Append(Pad((((VisualElement)b).parent == null) ? "(none)" : (string.IsNullOrEmpty(((VisualElement)b).parent.name) ? "(unnamed)" : ((VisualElement)b).parent.name), 25)); Rect worldBound = ((VisualElement)b).worldBound; StringBuilder stringBuilder2 = stringBuilder.Append(Pad(F(((Rect)(ref worldBound)).y), 7)); worldBound = ((VisualElement)b).worldBound; stringBuilder2.Append(Pad(F(((Rect)(ref worldBound)).height), 6)).Append(Pad(F(((VisualElement)b).resolvedStyle.marginTop), 6)).Append(Pad(F(((VisualElement)b).resolvedStyle.marginBottom), 6)) .Append(Safe(() => ((object)((VisualElement)b).resolvedStyle.display/*cast due to .constrained prefix*/).ToString())) .AppendLine(); } sb.AppendLine(); sb.AppendLine(" Quickstart's button is 'button_quickstart'; the Tutorial button is 'button_awakening' and Solo Play is 'button_solo'."); sb.AppendLine(" The shift is applied to the injected button's parent unless"); sb.AppendLine(" MenuLayout.SHIFT_TARGET_NAME names something else."); } private static void Collect(VisualElement ve, List<Button> into) { if (ve != null) { Button val = (Button)(object)((ve is Button) ? ve : null); if (val != null) { into.Add(val); } for (int i = 0; i < ve.childCount; i++) { Collect(ve[i], into); } } } private static string Classes(VisualElement ve) { StringBuilder stringBuilder = new StringBuilder(); try { foreach (string @class in ve.GetClasses()) { if (stringBuilder.Length > 0) { stringBuilder.Append(' '); } stringBuilder.Append(@class); } } catch { } return stringBuilder.ToString(); } private static string TextOf(VisualElement ve) { try { TextElement val = (TextElement)(object)((ve is TextElement) ? ve : null); if (val == null) { return null; } string text = val.text; if (string.IsNullOrEmpty(text)) { return null; } return text.Replace("\n", "\\n"); } catch { return null; } } private static string Safe(Func<string> f) { try { return f(); } catch { return "?"; } } private static string F(float v) { if (float.IsNaN(v)) { return "NaN"; } if (float.IsInfinity(v)) { return "inf"; } return v.ToString("0.##", CultureInfo.InvariantCulture); } private static string Ratio(int w, int h) { if (h <= 0) { return "?"; } return ((float)w / (float)h).ToString("0.###", CultureInfo.InvariantCulture) + ":1"; } private static string Pad(string s, int width) { if (s == null) { s = ""; } if (s.Length >= width) { return s + " "; } return s + new string(' ', width - s.Length); } } [BepInPlugin("jack.voidcrew.quickstart", "Quickstart", "1.0.1")] [BepInProcess("Void Crew.exe")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; private void Awake() { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; QuickstartConfig.Bind(((BaseUnityPlugin)this).Config); if (!QuickstartConfig.Enabled.Value) { Log.LogWarning((object)"Quickstart: disabled via config ([General] Enabled = false). No patches applied."); return; } Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "jack.voidcrew.quickstart"); GameObject val = new GameObject("Quickstart.Driver"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; QuickstartHubDriver.Attach(val); Log.LogInfo((object)"Quickstart v1.0.1 loaded."); } } public class QuickstartVoidPlugin : VoidPlugin { public override MultiplayerType MPType => (MultiplayerType)8; public override string Author => "Jack"; public override string Description => "Adds a Quickstart option to the main menu that drops you straight into a pilgrimage in the room kind and ship loadout you last used, skipping the hub entirely."; public override string ThunderstoreID => "Jack_Modding/Quickstart"; } public class QuickstartSettingsMenu : ModSettingsMenu { private static string _lastDump = ""; public override string Name() { return "Quickstart"; } public override void Draw() { GUILayout.Label("Status: " + QuickstartLauncher.Status, Array.Empty<GUILayoutOption>()); GUILayout.Label(VoiceReadiness.Describe(), Array.Empty<GUILayoutOption>()); GUILayout.Space(6f); GUILayout.Label("Quest: " + QuickstartConfig.QuestName.Value, Array.Empty<GUILayoutOption>()); GUILayout.Label("Ship: " + (QuickstartConfig.UseLastSelected.Value ? ("last selected (fallback " + QuickstartConfig.ShipName.Value + " / " + QuickstartConfig.LoadoutName.Value + ")") : (QuickstartConfig.ShipName.Value + " / " + QuickstartConfig.LoadoutName.Value)), Array.Empty<GUILayoutOption>()); GUILayout.Label("Session: " + SessionMemory.Describe(SessionMemory.Configured()) + " Room name: " + QuickstartConfig.RoomName.Value, Array.Empty<GUILayoutOption>()); GUILayout.Label("Skip hub: " + QuickstartConfig.SkipHub.Value + " Unlocked only: " + QuickstartConfig.UnlockedOnly.Value, Array.Empty<GUILayoutOption>()); GUILayout.Space(8f); QuickstartConfig.RememberSessionType.Value = GUILayout.Toggle(QuickstartConfig.RememberSessionType.Value, "Remember the last hosted run's kind (solo / private / public), player limit and name", Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Next launch:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) }); foreach (SessionKind value in Enum.GetValues(typeof(SessionKind))) { bool flag = SessionMemory.Configured() == value; if (GUILayout.Toggle(flag, value.ToString(), Array.Empty<GUILayoutOption>()) && !flag) { QuickstartConfig.SessionType.Value = value.ToString(); } } GUILayout.EndHorizontal(); if (SessionMemory.Configured() != SessionKind.Solo) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Player limit: " + QuickstartConfig.PlayerLimit.Value, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) }); QuickstartConfig.PlayerLimit.Value = (int)GUILayout.HorizontalSlider((float)QuickstartConfig.PlayerLimit.Value, 1f, 6f, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); GUILayout.EndHorizontal(); } GUILayout.Space(8f); QuickstartConfig.UseLastSelected.Value = GUILayout.Toggle(QuickstartConfig.UseLastSelected.Value, "Launch the ship loadout last selected at the hub (off = the Ship/Loadout names)", Array.Empty<GUILayoutOption>()); QuickstartConfig.SkipHub.Value = GUILayout.Toggle(QuickstartConfig.SkipHub.Value, "Skip the hub (off = load the station and auto-start from its terminals)", Array.Empty<GUILayoutOption>()); QuickstartConfig.UnlockedOnly.Value = GUILayout.Toggle(QuickstartConfig.UnlockedOnly.Value, "Only consider unlocked ship loadouts", Array.Empty<GUILayoutOption>()); QuickstartConfig.PlaceAboveSoloPlay.Value = GUILayout.Toggle(QuickstartConfig.PlaceAboveSoloPlay.Value, "Put the button above Solo Play (applies next time the menu is built)", Array.Empty<GUILayoutOption>()); QuickstartConfig.HideTutorial.Value = GUILayout.Toggle(QuickstartConfig.HideTutorial.Value, "Hide the Tutorial button (applies next time the menu is built)", Array.Empty<GUILayoutOption>()); GUILayout.Space(8f); GUILayout.Label("Names and paths only - nothing is loaded, so this is safe to press:", Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Dump available quests / ship loadouts to the log", Array.Empty<GUILayoutOption>())) { try { _lastDump = QuickstartAssets.Dump(); Plugin.Log.LogInfo((object)_lastDump); } catch (Exception ex) { _lastDump = "dump failed: " + ex.Message; Plugin.Log.LogError((object)("Quickstart: dump failed: " + ex)); } } if (!string.IsNullOrEmpty(_lastDump)) { GUILayout.Label(" (full text is in BepInEx/LogOutput.log)", Array.Empty<GUILayoutOption>()); string[] array = _lastDump.Split(new char[1] { '\n' }); int num = ((array.Length < 24) ? array.Length : 24); for (int i = 0; i < num; i++) { GUILayout.Label(array[i], Array.Empty<GUILayoutOption>()); } if (array.Length > num) { GUILayout.Label(" ... " + (array.Length - num) + " more", Array.Empty<GUILayoutOption>()); } } GUILayout.Space(8f); GUILayout.Label("Matching is case-insensitive over the asset FILENAME and RESOURCE PATH.", Array.Empty<GUILayoutOption>()); GUILayout.Label("Display names are deliberately not used: reading one goes through Unity", Array.Empty<GUILayoutOption>()); GUILayout.Label("Localization, which blocks, and doing it once per entry can freeze the game.", Array.Empty<GUILayoutOption>()); GUILayout.Label("Copy an exact filename - or a guid - into", Array.Empty<GUILayoutOption>()); GUILayout.Label("BepInEx/config/jack.voidcrew.quickstart.cfg.", Array.Empty<GUILayoutOption>()); if (!string.IsNullOrEmpty(QuickstartConfig.QuestGuid.Value)) { GUILayout.Label("QuestGuid pin: " + QuickstartConfig.QuestGuid.Value, Array.Empty<GUILayoutOption>()); } if (!string.IsNullOrEmpty(QuickstartConfig.LoadoutGuid.Value)) { GUILayout.Label("LoadoutGuid pin: " + QuickstartConfig.LoadoutGuid.Value, Array.Empty<GUILayoutOption>()); } MenuKit.DevZone("quickstart", DrawMenuLayoutSection); } private static void DrawMenuLayoutSection() { GUILayout.Label("--- main menu layout ---", Array.Empty<GUILayoutOption>()); GUILayout.Label("Layout: " + MenuLayout.Status, Array.Empty<GUILayoutOption>()); GUILayout.Label("Measured button slot: " + MenuLayout.MeasuredSlotPx.ToString("0.##") + " px applied shift: " + MenuLayout.AppliedShiftPx.ToString("0.##") + " px", Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Dump the main-menu visual tree (press this ON the main menu)", Array.Empty<GUILayoutOption>())) { try { string text = MenuTreeDump.Write(); MenuTreeDump.LastResult = "written to " + text; Plugin.Log.LogInfo((object)("Quickstart: menu tree dumped to " + text)); } catch (Exception ex) { MenuTreeDump.LastResult = "dump failed: " + ex.Message; Plugin.Log.LogWarning((object)("Quickstart: menu tree dump failed: " + ex)); } } if (!string.IsNullOrEmpty(MenuTreeDump.LastResult)) { GUILayout.Label(" " + MenuTreeDump.LastResult, Array.Empty<GUILayoutOption>()); } } } internal static class QuickstartPluginInfo { public const string PLUGIN_GUID = "jack.voidcrew.quickstart"; public const string PLUGIN_NAME = "Quickstart"; public const string USERS_PLUGIN_NAME = "Quickstart"; public const string PLUGIN_VERSION = "1.0.1"; public const string PLUGIN_DESCRIPTION = "Adds a Quickstart option to the main menu that drops you straight into a pilgrimage in the room kind and ship loadout you last used, skipping the hub entirely."; public const string PLUGIN_AUTHORS = "Jack"; public const string PLUGIN_THUNDERSTORE_ID = "Jack_Modding/Quickstart"; public const string BUTTON_NAME = "button_quickstart"; public const string SOLO_BUTTON_NAME = "button_solo"; public const string TUTORIAL_BUTTON_NAME = "button_awakening"; } internal static class QuickstartAssets { internal sealed class Candidate { public object Def; public GUIDUnion Guid; public string FileName = ""; public string Path = ""; public string Describe() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) return "file='" + FileName + "' path='" + Path + "' guid=" + Hex(Guid); } } private static bool _autoDumpedQuests; private static bool _autoDumpedLoadouts; internal static string Hex(GUIDUnion g) { try { return ((GUIDUnion)(ref g)).AsHex(); } catch { return "<?>"; } } private static List<TDef> Descriptions<TDef>(List<TDef> runtime, List<TDef> serialized) { if (runtime != null && runtime.Count > 0) { return runtime; } if (serialized != null) { return serialized; } return new List<TDef>(); } private static Candidate Describe(object def, GUIDUnion guid, ResourceAssetRef reference) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) Candidate candidate = new Candidate(); candidate.Def = def; candidate.Guid = guid; try { candidate.Path = reference.Path ?? ""; } catch { } try { candidate.FileName = reference.Filename ?? ""; } catch { } return candidate; } private static int Score(string needle, string fileName, string path) { if (string.IsNullOrEmpty(needle)) { return 1; } if (Eq(fileName, needle)) { return 100; } if (HasSegment(path, needle)) { return 80; } if (Has(fileName, needle)) { return 50; } if (Has(path, needle)) { return 30; } return 0; } private static bool Eq(string a, string b) { if (!string.IsNullOrEmpty(a)) { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } return false; } private static bool Has(string a, string b) { if (!string.IsNullOrEmpty(a)) { return a.IndexOf(b, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } private static bool HasSegment(string path, string needle) { if (string.IsNullOrEmpty(path)) { return false; } string[] array = path.Split('/', '\\'); for (int i = 0; i < array.Length; i++) { if (Eq(array[i], needle)) { return true; } } return false; } private static bool GuidMatches(string configuredHex, GUIDUnion guid) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(configuredHex)) { return false; } string b = configuredHex.Replace("-", "").Trim(); return string.Equals(Hex(guid), b, StringComparison.OrdinalIgnoreCase); } internal static List<Candidate> QuestCandidates() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) List<Candidate> list = new List<Candidate>(); QuestAssetContainer instance = ResourceAssetContainer<QuestAssetContainer, QuestAsset, QuestAssetDef>.Instance; if ((Object)(object)instance == (Object)null) { return list; } List<QuestAssetDef> list2 = Descriptions(((ResourceAssetContainer<QuestAssetContainer, QuestAsset, QuestAssetDef>)(object)instance).RuntimeDescriptions, ((ResourceAssetContainer<QuestAssetContainer, QuestAsset, QuestAssetDef>)(object)instance).AssetDescriptions); for (int i = 0; i < list2.Count; i++) { QuestAssetDef val = list2[i]; if (val != null && !(((ResourceAssetDef<QuestAsset>)(object)val).Ref == (ResourceAssetRef)null)) { list.Add(Describe(val, ((ResourceAssetDef<QuestAsset>)(object)val).AssetGuid, ((ResourceAssetDef<QuestAsset>)(object)val).Ref)); } } return list; } public static bool TryResolveQuest(out QuestAssetDef def, out string error) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Expected O, but got Unknown def = null; error = null; List<Candidate> list = QuestCandidates(); if (list.Count == 0) { error = (((Object)(object)ResourceAssetContainer<QuestAssetContainer, QuestAsset, QuestAssetDef>.Instance == (Object)null) ? "the quest container is not loaded yet - try again in a few seconds" : "the quest container is empty"); return false; } string value = QuickstartConfig.QuestGuid.Value; string value2 = QuickstartConfig.QuestName.Value; Candidate candidate = null; int num = 0; for (int i = 0; i < list.Count; i++) { Candidate candidate2 = list[i]; if (GuidMatches(value, candidate2.Guid)) { candidate = candidate2; num = int.MaxValue; break; } int num2 = Score(value2, candidate2.FileName, candidate2.Path); if (num2 > num) { num = num2; candidate = candidate2; } } if (candidate == null) { error = "no quest matched QuestName '" + value2 + "' among " + list.Count + " quests (the full list has been written to the log)"; AutoDump(ref _autoDumpedQuests, "quests", list); return false; } def = (QuestAssetDef)candidate.Def; Plugin.Log.LogDebug((object)("Quickstart: quest -> " + candidate.Describe() + " (type " + PeekQuestType(def) + ")")); return true; } private static string PeekQuestType(QuestAssetDef def) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 try { QuestAsset asset = ((ResourceAssetDef<QuestAsset>)(object)def).Asset; if ((Object)(object)asset == (Object)null) { return "asset not loadable from here"; } if ((int)asset.QuestType != 4) { Plugin.Log.LogWarning((object)("Quickstart: the chosen quest is type " + ((object)Unsafe.As<QuestType, QuestType>(ref asset.QuestType)/*cast due to .constrained prefix*/).ToString() + ", not Pilgrimage. Launching it anyway; set Selection/QuestName if that is not what you wanted.")); } return ((object)Unsafe.As<QuestType, QuestType>(ref asset.QuestType)/*cast due to .constrained prefix*/).ToString(); } catch (Exception ex) { return "unreadable (" + ex.Message + ")"; } } internal static List<Candidate> LoadoutCandidates(out HashSet<string> unlockedHex) { //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) unlockedHex = new HashSet<string>(); List<Candidate> list = new List<Candidate>(); ShipLoadoutDataContainer instance = ResourceAssetContainer<ShipLoadoutDataContainer, ShipLoadoutData, ShipLoadoutDataDef>.Instance; if ((Object)(object)instance == (Object)null) { return list; } try { List<ShipLoadoutDataDef> unlockedShipDefinitionsFromHighestRankPlayer = instance.GetUnlockedShipDefinitionsFromHighestRankPlayer(); if (unlockedShipDefinitionsFromHighestRankPlayer != null) { for (int i = 0; i < unlockedShipDefinitionsFromHighestRankPlayer.Count; i++) { if (unlockedShipDefinitionsFromHighestRankPlayer[i] != null) { unlockedHex.Add(Hex(((ResourceAssetDef<ShipLoadoutData>)(object)unlockedShipDefinitionsFromHighestRankPlayer[i]).AssetGuid)); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Quickstart: unlock lookup unavailable here (" + ex.Message + "); considering all loadouts.")); } List<ShipLoadoutDataDef> list2 = Descriptions(((ResourceAssetContainer<ShipLoadoutDataContainer, ShipLoadoutData, ShipLoadoutDataDef>)(object)instance).RuntimeDescriptions, ((ResourceAssetContainer<ShipLoadoutDataContainer, ShipLoadoutData, ShipLoadoutDataDef>)(object)instance).AssetDescriptions); for (int j = 0; j < list2.Count; j++) { ShipLoadoutDataDef val = list2[j]; if (val != null && !(((ResourceAssetDef<ShipLoadoutData>)(object)val).Ref == (ResourceAssetRef)null)) { list.Add(Describe(val, ((ResourceAssetDef<ShipLoadoutData>)(object)val).AssetGuid, ((ResourceAssetDef<ShipLoadoutData>)(object)val).Ref)); } } return list; } public static bool TryResolveLoadout(out ShipLoadoutDataDef def, out string error) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_026c: Unknown result type (might be due to invalid IL or missing references) //IL_0272: Expected O, but got Unknown //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) def = null; error = null; HashSet<string> unlockedHex; List<Candidate> list = LoadoutCandidates(out unlockedHex); if (list.Count == 0) { error = (((Object)(object)ResourceAssetContainer<ShipLoadoutDataContainer, ShipLoadoutData, ShipLoadoutDataDef>.Instance == (Object)null) ? "the ship loadout container is not loaded yet - try again in a few seconds" : "the ship loadout container is empty"); return false; } string value = QuickstartConfig.LoadoutGuid.Value; string value2 = QuickstartConfig.ShipName.Value; string value3 = QuickstartConfig.LoadoutName.Value; bool flag = QuickstartConfig.UnlockedOnly.Value && unlockedHex.Count > 0; Candidate candidate = null; int num = 0; bool flag2 = false; if (!string.IsNullOrEmpty(value)) { for (int i = 0; i < list.Count; i++) { if (candidate != null) { break; } if (GuidMatches(value, list[i].Guid)) { candidate = list[i]; num = int.MaxValue; } } } if (candidate == null && QuickstartConfig.UseLastSelected.Value) { string why; Candidate candidate2 = LastSelectedLoadout(list, out why); if (candidate2 == null) { Plugin.Log.LogInfo((object)("Quickstart: no remembered ship to use (" + why + "); falling back to ShipName/LoadoutName.")); } else if (flag && !unlockedHex.Contains(Hex(candidate2.Guid))) { Plugin.Log.LogWarning((object)("Quickstart: the remembered ship " + candidate2.Describe() + " is not unlocked on this profile; falling back to ShipName/LoadoutName.")); } else { candidate = candidate2; num = int.MaxValue; flag2 = true; Plugin.Log.LogDebug((object)"Quickstart: using the profile's remembered ship."); } } int num2 = 0; while (candidate == null && num2 < list.Count) { Candidate candidate3 = list[num2]; if (!flag || unlockedHex.Contains(Hex(candidate3.Guid))) { int num3 = Score(value3, candidate3.FileName, candidate3.Path); if (num3 != 0) { int num4 = (string.IsNullOrEmpty(value2) ? 1 : Score(value2, "", candidate3.Path)); int num5 = num3 * 1000 + num4 * 10; if (num5 > num) { num = num5; candidate = candidate3; flag2 = num4 > 1; } } } num2++; } if (candidate == null) { error = "no loadout matched LoadoutName '" + value3 + "' among " + list.Count + " loadouts" + (flag ? " (unlocked only)" : "") + " (the full list has been written to the log)"; AutoDump(ref _autoDumpedLoadouts, "ship loadouts", list); return false; } def = (ShipLoadoutDataDef)candidate.Def; string text = PeekShipName(def); Plugin.Log.LogDebug((object)("Quickstart: loadout -> " + candidate.Describe() + " (hull " + text + ")")); if (!string.IsNullOrEmpty(value2) && !flag2 && !Has(text, value2)) { Plugin.Log.LogWarning((object)("Quickstart: could not confirm this loadout belongs to ship '" + value2 + "' - its path does not mention it and its hull reads as '" + text + "'. Launching anyway; set Selection/LoadoutGuid to pin an exact one.")); } return true; } private static Candidate LastSelectedLoadout(List<Candidate> pool, out string why) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) why = null; try { if (!PlayerProfile.IsInitalized || PlayerProfile.Instance == null || PlayerProfile.Instance.PlayerLoadout == null) { why = "the player profile is not initialised yet"; return null; } ShipLoadoutDataContainer instance = ResourceAssetContainer<ShipLoadoutDataContainer, ShipLoadoutData, ShipLoadoutDataDef>.Instance; if ((Object)(object)instance == (Object)null) { why = "the ship loadout container is not loaded"; return null; } IPresentableResourceAsset loadoutItemForContainer = PlayerProfile.Instance.PlayerLoadout.GetLoadoutItemForContainer((IUnlockableDataContainer)(object)instance); if (loadoutItemForContainer == null) { why = "the profile has no ship recorded"; return null; } string text = Hex(((IResourceAsset)loadoutItemForContainer).AssetGuid); for (int i = 0; i < pool.Count; i++) { if (string.Equals(Hex(pool[i].Guid), text, StringComparison.OrdinalIgnoreCase)) { return pool[i]; } } why = "the profile's ship (guid " + text + ") is not in the loadout container"; return null; } catch (Exception ex) { why = "reading the profile threw: " + ex.Message; return null; } } private static string PeekShipName(ShipLoadoutDataDef def) { try { ShipLoadoutData asset = ((ResourceAssetDef<ShipLoadoutData>)(object)def).Asset; if ((Object)(object)asset == (Object)null || asset.ShipLoadout == null) { return "asset not loadable from here"; } ResourceAssetRef referenceShip = (ResourceAssetRef)(object)asset.ShipLoadout.ReferenceShip; if (referenceShip == (ResourceAssetRef)null || referenceShip.IsNull) { return "no reference ship"; } return referenceShip.Filename; } catch (Exception ex) { return "unreadable (" + ex.Message + ")"; } } private static void AutoDump(ref bool alreadyDone, string label, List<Candidate> pool) { if (!alreadyDone) { alreadyDone = true; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Quickstart: nothing matched, here is every " + label + " the container holds (" + pool.Count + "):"); Append(stringBuilder, pool); Plugin.Log.LogInfo((object)stringBuilder.ToString()); } } private static void Append(StringBuilder sb, List<Candidate> pool) { for (int i = 0; i < pool.Count; i++) { sb.AppendLine(" " + pool[i].Describe()); } } public static string Dump() { //IL_00d4: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("=== Quickstart: available assets (names and paths only, nothing loaded) ==="); try { List<Candidate> list = QuestCandidates(); stringBuilder.AppendLine("-- quests (" + list.Count + ") --"); Append(stringBuilder, list); } catch (Exception ex) { stringBuilder.AppendLine(" quest dump failed: " + ex); } try { HashSet<string> unlockedHex; List<Candidate> list2 = LoadoutCandidates(out unlockedHex); stringBuilder.AppendLine("-- ship loadouts (" + list2.Count + ", " + unlockedHex.Count + " reported unlocked) --"); for (int i = 0; i < list2.Count; i++) { stringBuilder.AppendLine(" " + (unlockedHex.Contains(Hex(list2[i].Guid)) ? "[U] " : "[ ] ") + list2[i].Describe()); } } catch (Exception ex2) { stringBuilder.AppendLine(" loadout dump failed: " + ex2); } stringBuilder.AppendLine("Paste an exact filename into Selection/QuestName or Selection/LoadoutName, or a guid into Selection/QuestGuid or Selection/LoadoutGuid."); return stringBuilder.ToString(); } } internal static class QuickstartConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<string> ButtonLabel; public static ConfigEntry<bool> PlaceAboveSoloPlay; public static ConfigEntry<bool> HideTutorial; public static ConfigEntry<string> QuestName; public static ConfigEntry<bool> UseLastSelected; public static ConfigEntry<string> ShipName; public static ConfigEntry<string> LoadoutName; public static ConfigEntry<string> QuestGuid; public static ConfigEntry<string> LoadoutGuid; public static ConfigEntry<bool> UnlockedOnly; public static ConfigEntry<bool> SkipHub; public static ConfigEntry<float> VoiceReadyTimeout; public static ConfigEntry<bool> RememberSessionType; public static ConfigEntry<string> SessionType; public static ConfigEntry<int> PlayerLimit; public static ConfigEntry<string> RoomName; public static void Bind(ConfigFile cfg) { //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Expected O, but got Unknown //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Expected O, but got Unknown Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. False = no button injected, no patches do anything."); ButtonLabel = cfg.Bind<string>("Button", "Label", "QUICKSTART", "Text shown on the injected main-menu button."); PlaceAboveSoloPlay = cfg.Bind<bool>("Button", "PlaceAboveSoloPlay", false, "True puts Quickstart directly above Solo Play; false (default) puts it directly below."); HideTutorial = cfg.Bind<bool>("Button", "HideTutorial", true, "True (default) removes the Tutorial button so Quickstart takes its slot and the menu keeps its stock size. Turn off on a profile that has not completed the Awakening yet - the game will not host a multiplayer room until it has."); QuestName = cfg.Bind<string>("Selection", "QuestName", "Pilgrimage", "Which quest to launch, matched against the quest asset's filename and resource path. If nothing matches, the log lists every quest available at the moment of failure - copy an exact name out of it. The mod settings menu's dump button prints the same list on demand."); UseLastSelected = cfg.Bind<bool>("Selection", "UseLastSelected", true, "True (default) launches the ship loadout the profile remembers - the one last picked at the hub's ship terminal, or last launched with. Falls back to ShipName/LoadoutName when the profile has nothing usable recorded, or when the remembered ship is not unlocked and UnlockedOnly is on. False always uses ShipName/LoadoutName."); ShipName = cfg.Bind<string>("Selection", "ShipName", "Frigate", "Preferred ship hull, matched against the loadout's resource path. Used when UseLastSelected is off or the profile has no usable ship recorded. This is a preference, not a filter - if no path mentions the hull, the best LoadoutName match still wins and the log says the hull could not be confirmed. Blank = ignore."); LoadoutName = cfg.Bind<string>("Selection", "LoadoutName", "Freedom", "Which loadout to launch with, matched against the loadout asset's filename and resource path. Used when UseLastSelected is off or the profile has no usable ship recorded."); QuestGuid = cfg.Bind<string>("Selection", "QuestGuid", "", "Escape hatch: a 32-char hex asset GUID that overrides QuestName completely. The dump in the F5 menu prints the guid of every quest."); LoadoutGuid = cfg.Bind<string>("Selection", "LoadoutGuid", "", "Escape hatch: a 32-char hex asset GUID that overrides UseLastSelected and ShipName/LoadoutName completely, and bypasses the unlocked-only check."); UnlockedOnly = cfg.Bind<bool>("Selection", "UnlockedOnly", true, "True (default) only considers loadouts the profile has actually unlocked, so Quickstart can never hand you a ship the hub terminal would refuse. The unlock list reads player rank, which is a session concept, so when it comes back empty in the main menu this is ignored rather than blocking the launch."); SkipHub = cfg.Bind<bool>("Launch", "SkipHub", true, "True (default) builds the mission session in the main menu and loads straight into the pilgrimage. False loads the hub as normal and then drives the hub's own ship-select / quest-select / start-countdown for you - slower, but it goes through exactly the code path the game uses, so it is the fallback if the direct route ever breaks."); VoiceReadyTimeout = cfg.Bind<float>("Launch", "VoiceReadyTimeoutSeconds", 20f, "The button stays disabled until the game's voice chat has finished its start-up login, since launching over it leaves in-game voice dead for the run. This is how long to wait before giving up and enabling the button anyway. 0 skips the wait entirely."); RememberSessionType = cfg.Bind<bool>("Launch", "RememberSessionType", true, "True (default) records the kind of room you last started a run in as host - solo, hosted private or hosted public - with its player limit and room name, into SessionType/PlayerLimit/RoomName, so the next Quickstart opens the same kind. False leaves those three keys as you set them."); SessionType = cfg.Bind<string>("Launch", "SessionType", SessionKind.Solo.ToString(), new ConfigDescription("What kind of room Quickstart opens. Solo = private, one player, no crossplay, exactly like the Solo Play button. HostedPrivate / HostedPublic = the Host panel's private / public game, with PlayerLimit and the game's own crossplay setting. Overwritten by the last hosted run while RememberSessionType is on.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { SessionKind.Solo.ToString(), SessionKind.HostedPrivate.ToString(), SessionKind.HostedPublic.ToString() }), Array.Empty<object>())); PlayerLimit = cfg.Bind<int>("Launch", "PlayerLimit", 6, new ConfigDescription("Player limit for a hosted room, as on the Host panel's slider. Ignored for Solo, which is always 1.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 6), Array.Empty<object>())); RoomName = cfg.Bind<string>("Launch", "RoomName", "Quickstart", "Photon room name. Cosmetic for Solo; for a hosted room it is the name other players see in the browser. Overwritten by the last hosted run while RememberSessionType is on."); } } internal class QuickstartHubDriver : MonoBehaviour { private enum Step { Idle, WaitForHub, SelectShip, SelectQuest, StartQuest, Done } private static QuickstartHubDriver _instance; private static bool _armed; private Step _step; private float _nextActionTime; private float _giveUpTime; private bool _loggedFailure; public static void Arm() { _armed = true; if ((Object)(object)_instance != (Object)null) { _instance.Begin(); } } public static void Attach(GameObject host) { if ((Object)(object)_instance == (Object)null) { _instance = host.AddComponent<QuickstartHubDriver>(); } } private void Awake() { _instance = this; if (_armed) { Begin(); } } private void Begin() { _step = Step.WaitForHub; _nextActionTime = 0f; _giveUpTime = Time.unscaledTime + 120f; _loggedFailure = false; } private void Update() { if (_step == Step.Idle || _step == Step.Done) { return; } try { if (Time.unscaledTime > _giveUpTime) { Finish("timed out waiting for the hub; you are in the station, start the run from the terminals as normal"); } else if (!(Time.unscaledTime < _nextActionTime)) { switch (_step) { case Step.WaitForHub: TickWaitForHub(); break; case Step.SelectShip: TickSelectShip(); break; case Step.SelectQuest: TickSelectQuest(); break; case Step.StartQuest: TickStartQuest(); break; } } } catch (Exception ex) { if (!_loggedFailure) { _loggedFailure = true; Plugin.Log.LogError((object)("Quickstart: hub auto-start failed at " + _step.ToString() + ": " + ex)); } Finish("hub auto-start failed at " + _step.ToString() + " (see the log); the station is playable as normal"); } } private void TickWaitForHub() { if (!((Object)(object)HubQuestManager.Instance == (Object)null) && !((Object)(object)HubShipManager.Instance == (Object)null) && PhotonNetwork.IsMasterClient && !((Object)(object)GameSessionManager.Instance == (Object)null) && GameSessionManager.HasActiveSession && GameSessionManager.InHub && Traverse.Create((object)HubQuestManager.Instance).Field("questGameSession").GetValue() != null) { _step = Step.SelectShip; _nextActionTime = Time.unscaledTime + 0.75f; } } private void TickSelectShip() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) ShipLoadoutDataDef pendingLoadout = QuickstartLauncher.PendingLoadout; if (pendingLoadout == null) { Finish("no loadout was pending"); return; } HubShipManager instance = HubShipManager.Instance; if ((Object)(object)instance == (Object)null || !GameSessionManager.InHub) { Finish("left the hub before the ship could be selected"); return; } GUIDUnion assetGuid = ((ResourceAssetDef<ShipLoadoutData>)(object)pendingLoadout).AssetGuid; instance.SelectShip(((GUIDUnion)(ref assetGuid)).AsIntArray()); QuickstartLauncher.Status = "hub: selected " + QuickstartLauncher.Describe(pendingLoadout); Plugin.Log.LogDebug((object)("Quickstart: hub ship selected -> " + QuickstartLauncher.Describe(pendingLoadout))); _step = Step.SelectQuest; _nextActionTime = Time.unscaledTime + 0.75f; } private void TickSelectQuest() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) QuestAssetDef pendingQuest = QuickstartLauncher.PendingQuest; if (pendingQuest == null) { Finish("no quest was pending"); return; } HubQuestManager instance = HubQuestManager.Instance; if ((Object)(object)instance == (Object)null || !GameSessionManager.InHub) { Finish("left the hub before the quest could be selected"); return; } instance.SelectQuest(((ResourceAssetDef<QuestAsset>)(object)pendingQuest).AssetGuid); QuickstartLauncher.Status = "hub: selected " + QuickstartLauncher.Describe(pendingQuest); Plugin.Log.LogDebug((object)("Quickstart: hub quest selected -> " + QuickstartLauncher.Describe(pendingQuest))); _step = Step.StartQuest; _nextActionTime = Time.unscaledTime + 1f; } private void TickStartQuest() { HubQuestManager instance = HubQuestManager.Instance; if ((Object)(object)instance == (Object)null) { Finish("hub quest manager vanished"); return; } if ((ResourceAssetRef)(object)instance.SelectedQuest == (ResourceAssetRef)null) { _nextActionTime = Time.unscaledTime + 0.5f; return; } Traverse obj = Traverse.Create((object)instance); int value = obj.Field("questSeed").GetValue<int>(); int value2 = obj.Field("challengeSeed").GetValue<int>(); instance.StartQuest(instance.SelectedQuest, (SessionStartType)1, value, value2); QuickstartLauncher.Status = "hub: launching pilgrimage"; Plugin.Log.LogInfo((object)("Quickstart: hub StartQuest issued (seed " + value + ").")); _step = Step.Done; _armed = false; } private void Finish(string message) { QuickstartLauncher.Status = message; Plugin.Log.LogWarning((object)("Quickstart: " + message)); _step = Step.Done; _armed = false; } } internal static class QuickstartLauncher { public static string Status = "idle"; internal static QuestAssetDef PendingQuest; internal static ShipLoadoutDataDef PendingLoadout; private static SessionKind _pendingKind; private static bool _pendingCrossplay; private static int _pendingLimit; public static bool InFlight { get; private set; } public static void Launch() { if (InFlight) { Plugin.Log.LogDebug((object)"Quickstart: launch already in progress, ignoring."); return; } if (!MenuInjection.IsReadyToLaunch()) { Fail("not connected to the Photon lobby yet - give it a moment and try again"); return; } if (!QuickstartAssets.TryResolveQuest(out var def, out var error)) { Fail("could not pick a quest - " + error); return; } if (!QuickstartAssets.TryResolveLoadout(out var def2, out error)) { Fail("could not pick a ship loadout - " + error); return; } PendingQuest = def; PendingLoadout = def2; ResolveRoomShape(); Plugin.Log.LogInfo((object)("Quickstart: launching quest '" + Describe(def) + "' with loadout '" + Describe(def2) + "' as " + SessionMemory.Describe(_pendingKind) + (_pendingCrossplay ? ", crossplay on" : "") + " (SkipHub=" + QuickstartConfig.SkipHub.Value + ").")); InFlight = true; Status = "creating " + SessionMemory.Describe(_pendingKind) + " room..."; try { PunSingleton<PhotonService>.I.CreateRoom(_pendingKind == SessionKind.HostedPublic, _pendingCrossplay).Then((Action)OnRoomCreated).Catch((Action<Exception>)OnLaunchFailed); } catch (Exception ex) { InFlight = false; Fail("CreateRoom threw: " + ex.Message); Plugin.Log.LogError((object)("Quickstart: CreateRoom threw: " + ex)); } } private static void ResolveRoomShape() { _pendingKind = SessionMemory.Configured(); _pendingCrossplay = false; _pendingLimit = 1; if (_pendingKind == SessionKind.Solo) { return; } try { if (!PlayerProfile.PlayerData.CompletedAwakening && !RuntimeSettings.LanPlay) { Plugin.Log.LogWarning((object)"Quickstart: the Awakening is not complete, so the game will not host a multiplayer room yet; launching solo."); _pendingKind = SessionKind.Solo; return; } if (!PAL.Privileges.HasMultiplayerPrivilege(true)) { Plugin.Log.LogWarning((object)"Quickstart: no multiplayer privilege on this platform; launching solo."); _pendingKind = SessionKind.Solo; return; } _pendingLimit = Math.Max(1, Math.Min(6, QuickstartConfig.PlayerLimit.Value)); bool flag = SettingsManager.CurrentSettings.GeneralSettings.EnableCrossPlay.ValueAsBool; if (flag && !PAL.Privileges.HasCrossPlayPrivilege(true)) { Plugin.Log.LogWarning((object)"Quickstart: crossplay is on in settings but the platform denies it; hosting without crossplay."); flag = false; } _pendingCrossplay = flag; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Quickstart: could not check hosting eligibility (" + ex.Message + "); launching solo.")); _pendingKind = SessionKind.Solo; _pendingCrossplay = false; _pendingLimit = 1; } } private static void OnRoomCreated() { try { bool flag = _pendingKind == SessionKind.HostedPublic; try { PAL.Matchmaking.CreateLobby(flag); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Quickstart: platform CreateLobby failed: " + ex.Message)); } PunSingleton<PhotonService>.I.SetCurrentRoomPlayerLimit(_pendingLimit); string value = QuickstartConfig.RoomName.Value; if (!string.IsNullOrEmpty(value)) { PunSingleton<PhotonService>.I.SetCurrentRoomName(value); } if (QuickstartConfig.SkipHub.Value) { LaunchMissionDirectly(); } else { LaunchViaHub(); } } catch (Exception ex2) { InFlight = false; Fail("room setup failed: " + ex2.Message); Plugin.Log.LogError((object)("