Decompiled source of Speedometer v1.0.0

Speedometer.dll

Decompiled a day ago
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.Game.Player;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ResourceAssets;
using UI.MainHUD;
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("Speedometer")]
[assembly: AssemblyDescription("Adds a third HUD readout, in the same style as the O2 and Temperature widgets, showing your current speed in m/s.")]
[assembly: AssemblyProduct("Speedometer")]
[assembly: AssemblyCompany("Jack")]
[assembly: AssemblyFileVersion("1.0.0.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.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
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 Speedometer
{
	[BepInPlugin("jack.voidcrew.speedometer", "Speedometer", "1.0.0")]
	[BepInProcess("Void Crew.exe")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> ShowSpeedometer;

		internal static ConfigEntry<float> RowGapPx;

		internal static ConfigEntry<bool> UseRollingAverage;

		internal static ConfigEntry<int> RollingAverageTicks;

		private void Awake()
		{
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a0: Expected O, but got Unknown
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			ShowSpeedometer = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowSpeedometer", true, "Show the speed (m/s) readout on the HUD, next to O2 and Temperature.");
			RowGapPx = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "RowGapPx", -1f, "Optional extra horizontal gap between the O2/Temperature/Speed row items, in pixels, on top of the default spacing. -1 (default) adds nothing extra. Takes effect live; no restart needed.");
			UseRollingAverage = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "UseRollingAverage", false, "Show a rolling average over the last RollingAverageTicks frames instead of the instantaneous speed.");
			RollingAverageTicks = ((BaseUnityPlugin)this).Config.Bind<int>("UI", "RollingAverageTicks", 30, "Window size, in ticks (Update() frames), for the rolling average when UseRollingAverage is on. Clamped to [1, 600]. Higher = smoother but laggier.");
			GameObject val = new GameObject("Speedometer.Driver");
			Object.DontDestroyOnLoad((Object)val);
			((Object)val).hideFlags = (HideFlags)61;
			val.AddComponent<SpeedometerDriver>();
			Log.LogInfo((object)"Speedometer v1.0.0 loaded.");
		}
	}
	public class SpeedometerVoidPlugin : VoidPlugin
	{
		public override MultiplayerType MPType => (MultiplayerType)8;

		public override string Author => "Jack";

		public override string Description => "Adds a third HUD readout, in the same style as the O2 and Temperature widgets, showing your current speed in m/s.";

		public override string ThunderstoreID => "Jack_Modding/Speedometer";
	}
	public class SpeedometerSettingsMenu : ModSettingsMenu
	{
		private static string lastDump = "";

		public override string Name()
		{
			return "Speedometer";
		}

		public override void Draw()
		{
			Plugin.ShowSpeedometer.Value = GUILayout.Toggle(Plugin.ShowSpeedometer.Value, "Show speed (m/s) on the HUD", Array.Empty<GUILayoutOption>());
			GUILayout.Space(6f);
			GUILayout.Label("Row gap override (px, -1 = leave authored spacing alone):", Array.Empty<GUILayoutOption>());
			if (float.TryParse(GUILayout.TextField(Plugin.RowGapPx.Value.ToString("0.##"), Array.Empty<GUILayoutOption>()), out var result))
			{
				Plugin.RowGapPx.Value = result;
			}
			GUILayout.Space(6f);
			Plugin.UseRollingAverage.Value = GUILayout.Toggle(Plugin.UseRollingAverage.Value, "Smooth reading: rolling average over the last N ticks (instead of instant)", Array.Empty<GUILayoutOption>());
			if (Plugin.UseRollingAverage.Value)
			{
				GUILayout.Label("Window size N (ticks):", Array.Empty<GUILayoutOption>());
				if (int.TryParse(GUILayout.TextField(Plugin.RollingAverageTicks.Value.ToString(), Array.Empty<GUILayoutOption>()), out var result2))
				{
					Plugin.RollingAverageTicks.Value = Mathf.Clamp(result2, 1, 600);
				}
			}
			MenuKit.DevZone("speedometer", DrawDevTools);
		}

		private static void DrawDevTools()
		{
			if (GUILayout.Button("Dump HUD cluster to log (O2/Temperature/HP + our element)", Array.Empty<GUILayoutOption>()))
			{
				try
				{
					lastDump = SpeedometerHudController.DumpTree();
					Plugin.Log.LogInfo((object)lastDump);
				}
				catch (Exception ex)
				{
					lastDump = "dump failed: " + ex.Message;
					Plugin.Log.LogError((object)("Speedometer: 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 < 20) ? array.Length : 20);
				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>());
				}
			}
		}
	}
	internal static class SpeedometerPluginInfo
	{
		public const string PLUGIN_GUID = "jack.voidcrew.speedometer";

		public const string PLUGIN_NAME = "Speedometer";

		public const string USERS_PLUGIN_NAME = "Speedometer";

		public const string PLUGIN_VERSION = "1.0.0";

		public const string PLUGIN_DESCRIPTION = "Adds a third HUD readout, in the same style as the O2 and Temperature widgets, showing your current speed in m/s.";

		public const string PLUGIN_AUTHORS = "Jack";

		public const string PLUGIN_THUNDERSTORE_ID = "Jack_Modding/Speedometer";
	}
	internal static class SpeedIcon
	{
		private const int FallbackSize = 64;

		private const string ResourceName = "Speedometer.assets.speedometer-icon.png";

		private static Texture2D cached;

		private static bool buildFailed;

		internal static Texture2D GetTexture()
		{
			if ((Object)(object)cached != (Object)null || buildFailed)
			{
				return cached;
			}
			try
			{
				cached = LoadPackaged() ?? BuildFallback();
			}
			catch (Exception ex)
			{
				buildFailed = true;
				Plugin.Log.LogWarning((object)("Speedometer: icon load failed, showing no glyph: " + ex));
			}
			return cached;
		}

		private static Texture2D LoadPackaged()
		{
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Expected O, but got Unknown
			Assembly executingAssembly = Assembly.GetExecutingAssembly();
			string name = "Speedometer.assets.speedometer-icon.png";
			bool flag = false;
			string[] manifestResourceNames = executingAssembly.GetManifestResourceNames();
			for (int i = 0; i < manifestResourceNames.Length; i++)
			{
				if (string.Equals(manifestResourceNames[i], "Speedometer.assets.speedometer-icon.png", StringComparison.OrdinalIgnoreCase))
				{
					flag = true;
					break;
				}
			}
			if (!flag)
			{
				manifestResourceNames = executingAssembly.GetManifestResourceNames();
				foreach (string text in manifestResourceNames)
				{
					if (text.EndsWith("speedometer-icon.png", StringComparison.OrdinalIgnoreCase))
					{
						name = text;
						flag = true;
						break;
					}
				}
			}
			if (!flag)
			{
				Plugin.Log.LogWarning((object)"Speedometer: embedded icon resource not found - falling back to the procedural glyph.");
				return null;
			}
			byte[] array;
			using (Stream stream = executingAssembly.GetManifestResourceStream(name))
			{
				using MemoryStream memoryStream = new MemoryStream();
				stream.CopyTo(memoryStream);
				array = memoryStream.ToArray();
			}
			Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false)
			{
				name = "SpeedometerIcon",
				filterMode = (FilterMode)1,
				wrapMode = (TextureWrapMode)1
			};
			if (!ImageConversion.LoadImage(val, array))
			{
				Plugin.Log.LogWarning((object)"Speedometer: could not decode embedded icon - falling back to the procedural glyph.");
				return null;
			}
			return val;
		}

		private static Texture2D BuildFallback()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Expected O, but got Unknown
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: 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)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: 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)
			//IL_0123: Unknown result type (might be due to invalid IL or missing references)
			//IL_0183: Unknown result type (might be due to invalid IL or missing references)
			//IL_0188: Unknown result type (might be due to invalid IL or missing references)
			Texture2D val = new Texture2D(64, 64, (TextureFormat)4, false)
			{
				name = "SpeedometerIconFallback",
				filterMode = (FilterMode)1,
				wrapMode = (TextureWrapMode)1
			};
			Vector2[][] array = new Vector2[2][];
			for (int i = 0; i < 2; i++)
			{
				float num = 64f * ((i == 0) ? 0.46f : 0.74f);
				float num2 = 32f;
				float num3 = 14.08f;
				float num4 = 19.2f;
				array[i] = (Vector2[])(object)new Vector2[3]
				{
					new Vector2(num - num3, num2 - num4),
					new Vector2(num, num2),
					new Vector2(num - num3, num2 + num4)
				};
			}
			Color32[] array2 = (Color32[])(object)new Color32[4096];
			for (int j = 0; j < 64; j++)
			{
				for (int k = 0; k < 64; k++)
				{
					float px = (float)k + 0.5f;
					float py = (float)j + 0.5f;
					float num5 = float.MaxValue;
					Vector2[][] array3 = array;
					foreach (Vector2[] array4 in array3)
					{
						num5 = Mathf.Min(num5, DistanceToSegment(px, py, array4[0], array4[1]));
						num5 = Mathf.Min(num5, DistanceToSegment(px, py, array4[1], array4[2]));
					}
					float num6 = Mathf.Clamp01(0.5f - (num5 - 5.12f) / 1.5f);
					array2[j * 64 + k] = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)Mathf.RoundToInt(num6 * 255f));
				}
			}
			val.SetPixels32(array2);
			val.Apply(false, false);
			return val;
		}

		private static float DistanceToSegment(float px, float py, Vector2 a, Vector2 b)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: 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)
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: 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)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			Vector2 val = b - a;
			float num = Mathf.Clamp01(Vector2.Dot(new Vector2(px, py) - a, val) / Mathf.Max(((Vector2)(ref val)).sqrMagnitude, 0.0001f));
			Vector2 val2 = a + val * num;
			return Vector2.Distance(new Vector2(px, py), val2);
		}
	}
	internal class SpeedometerDriver : MonoBehaviour
	{
		private void Update()
		{
			SpeedometerHudController.Tick();
		}
	}
	internal static class SpeedometerHudController
	{
		private const string MarkerClass = "speedometer-resource";

		private const string ElementName = "SpeedometerResource";

		private const float FallbackGap = 6f;

		private static readonly FieldInfo ContainerF = AccessTools.Field(typeof(HUDElement), "container");

		private static PlayerHUD hud;

		private static VisualElement container;

		private static VisualElement stackParent;

		private static VisualElement anchor;

		private static VisualElement oxygenAnchor;

		private static readonly VisualElement[] leftSpacers = (VisualElement[])(object)new VisualElement[2];

		private static HUDResourceVE speedVE;

		private static bool usingFlexInsert;

		private static float nextSearchTime;

		private static float nextPlaceTime;

		private static float nextValueTime;

		private static int lastShownSpeed = int.MinValue;

		private static float appliedGapPx = float.NaN;

		private static float appliedEqualWidth = float.NaN;

		private static bool tickErrorLogged;

		private static bool templateWarned;

		private static bool anchorsWarned;

		private static float[] sampleBuffer;

		private static int sampleCount;

		private static int sampleIndex;

		private static float sampleSum;

		private static int appliedCapacity = -1;

		internal static void Tick()
		{
			try
			{
				TickInner();
			}
			catch (Exception ex)
			{
				if (!tickErrorLogged)
				{
					tickErrorLogged = true;
					Plugin.Log.LogError((object)("Speedometer: tick error (ticks continue; logged once): " + ex));
				}
			}
		}

		private static void TickInner()
		{
			if (!Plugin.ShowSpeedometer.Value)
			{
				Unwire();
				return;
			}
			if (speedVE == null || (Object)(object)hud == (Object)null)
			{
				if (Time.unscaledTime < nextSearchTime)
				{
					return;
				}
				nextSearchTime = Time.unscaledTime + 1f;
				if (!TryWire())
				{
					return;
				}
			}
			EqualizeWidths();
			ApplyGapOverride();
			EnsureSampleCapacity();
			PushSample(ReadSpeed());
			UpdateValue();
			if (!usingFlexInsert)
			{
				Place();
			}
		}

		private static bool TryWire()
		{
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Unknown result type (might be due to invalid IL or missing references)
			Unwire();
			hud = Object.FindObjectOfType<PlayerHUD>();
			if ((Object)(object)hud == (Object)null)
			{
				return false;
			}
			object? value = ContainerF.GetValue(hud);
			container = (VisualElement)((value is VisualElement) ? value : null);
			if (container == null)
			{
				hud = null;
				return false;
			}
			VisualElement val = UQueryExtensions.Q(container, "OxygenResource", (string)null);
			VisualElement val2 = UQueryExtensions.Q(container, "TemperatureResource", (string)null);
			if (val == null || val2 == null)
			{
				if (!anchorsWarned)
				{
					anchorsWarned = true;
					Plugin.Log.LogWarning((object)"Speedometer: LocalPlayerContainer has no OxygenResource/TemperatureResource to anchor on (game update?). Icon stays hidden.");
				}
				hud = null;
				container = null;
				return false;
			}
			oxygenAnchor = val;
			anchor = val2;
			VisualElement obj = BuildElement();
			speedVE = (HUDResourceVE)(object)((obj is HUDResourceVE) ? obj : null);
			if (speedVE == null)
			{
				hud = null;
				container = null;
				return false;
			}
			if (val2.parent != null && val2.parent == val.parent)
			{
				stackParent = val2.parent;
				stackParent.style.alignSelf = StyleEnum<Align>.op_Implicit((Align)1);
				BuildLeftSpacers(stackParent, val);
				int num = stackParent.IndexOf(val2);
				stackParent.Insert(num + 1, (VisualElement)(object)speedVE);
				usingFlexInsert = true;
				((VisualElement)speedVE).style.position = StyleEnum<Position>.op_Implicit((Position)0);
			}
			else
			{
				container.Add((VisualElement)(object)speedVE);
				usingFlexInsert = false;
				((VisualElement)speedVE).style.position = StyleEnum<Position>.op_Implicit((Position)1);
			}
			speedVE.Unit = " m/s";
			SetIgnorePicking((VisualElement)(object)speedVE);
			ApplyIcon((VisualElement)(object)speedVE);
			appliedGapPx = float.NaN;
			appliedEqualWidth = float.NaN;
			lastShownSpeed = int.MinValue;
			tickErrorLogged = false;
			nextPlaceTime = 0f;
			nextValueTime = 0f;
			Plugin.Log.LogInfo((object)("Speedometer: HUD element built (" + (usingFlexInsert ? "inserted into the O2/Temperature stack" : "absolute-positioned fallback") + ")."));
			return true;
		}

		private static VisualElement BuildElement()
		{
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			UIDocument component = ((Component)hud).GetComponent<UIDocument>();
			if ((Object)(object)component == (Object)null || (Object)(object)component.visualTreeAsset == (Object)null)
			{
				return null;
			}
			VisualElement val = UQueryExtensions.Q((VisualElement)(object)component.visualTreeAsset.Instantiate(), "LocalPlayerContainer", (string)null);
			VisualElement val2 = ((val != null) ? UQueryExtensions.Q(val, "TemperatureResource", (string)null) : null);
			if (val2 == null)
			{
				if (!templateWarned)
				{
					templateWarned = true;
					Plugin.Log.LogWarning((object)"Speedometer: the HUD document has no LocalPlayerContainer/TemperatureResource template to copy - element skipped this session.");
				}
				return null;
			}
			val2.RemoveFromHierarchy();
			val2.name = "SpeedometerResource";
			val2.AddToClassList("speedometer-resource");
			val2.style.display = StyleEnum<DisplayStyle>.op_Implicit((DisplayStyle)0);
			val2.style.visibility = StyleEnum<Visibility>.op_Implicit((Visibility)0);
			val2.style.opacity = StyleFloat.op_Implicit(1f);
			return val2;
		}

		private static void BuildLeftSpacers(VisualElement parent, VisualElement oxygenRes)
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Expected O, but got Unknown
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			VisualElement val = UQueryExtensions.Q(oxygenRes, "IconBackground", (string)null);
			if (val == null)
			{
				return;
			}
			int num = parent.IndexOf(oxygenRes);
			for (int i = 0; i < leftSpacers.Length; i++)
			{
				VisualElement val2 = new VisualElement
				{
					name = "SpeedometerLeftSpacer" + i
				};
				foreach (string @class in val.GetClasses())
				{
					val2.AddToClassList(@class);
				}
				val2.style.visibility = StyleEnum<Visibility>.op_Implicit((Visibility)1);
				val2.pickingMode = (PickingMode)1;
				parent.Insert(num, val2);
				leftSpacers[i] = val2;
			}
		}

		private static void ApplyIcon(VisualElement ve)
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			VisualElement val = UQueryExtensions.Q(ve, "Icon", (string)null);
			if (val != null)
			{
				Texture2D texture = SpeedIcon.GetTexture();
				if (!((Object)(object)texture == (Object)null))
				{
					val.style.backgroundImage = new StyleBackground(texture);
				}
			}
		}

		private static void SetIgnorePicking(VisualElement ve)
		{
			ve.pickingMode = (PickingMode)1;
			foreach (VisualElement item in ve.Children())
			{
				SetIgnorePicking(item);
			}
		}

		private static void EqualizeWidths()
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			if (usingFlexInsert && oxygenAnchor != null && anchor != null && speedVE != null)
			{
				float width = oxygenAnchor.resolvedStyle.width;
				if (!float.IsNaN(width) && !(width <= 0f) && width != appliedEqualWidth)
				{
					appliedEqualWidth = width;
					anchor.style.width = StyleLength.op_Implicit(width);
					((VisualElement)speedVE).style.width = StyleLength.op_Implicit(width);
				}
			}
		}

		private static void ApplyGapOverride()
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			if (!usingFlexInsert)
			{
				return;
			}
			float value = Plugin.RowGapPx.Value;
			if (value != appliedGapPx)
			{
				appliedGapPx = value;
				if (float.IsNaN(value) || value < 0f)
				{
					oxygenAnchor.style.marginRight = StyleLength.op_Implicit((StyleKeyword)1);
					anchor.style.marginRight = StyleLength.op_Implicit((StyleKeyword)1);
					((VisualElement)speedVE).style.marginRight = StyleLength.op_Implicit((StyleKeyword)1);
				}
				else
				{
					oxygenAnchor.style.marginRight = StyleLength.op_Implicit(value);
					anchor.style.marginRight = StyleLength.op_Implicit(value);
					((VisualElement)speedVE).style.marginRight = StyleLength.op_Implicit(value);
				}
			}
		}

		private static void EnsureSampleCapacity()
		{
			int num = Mathf.Clamp(Plugin.RollingAverageTicks.Value, 1, 600);
			if (num != appliedCapacity)
			{
				appliedCapacity = num;
				sampleBuffer = new float[num];
				sampleCount = 0;
				sampleIndex = 0;
				sampleSum = 0f;
			}
		}

		private static void PushSample(float speed)
		{
			if (sampleCount < sampleBuffer.Length)
			{
				sampleBuffer[sampleIndex] = speed;
				sampleSum += speed;
				sampleCount++;
			}
			else
			{
				sampleSum += speed - sampleBuffer[sampleIndex];
				sampleBuffer[sampleIndex] = speed;
			}
			sampleIndex = (sampleIndex + 1) % sampleBuffer.Length;
		}

		private static void UpdateValue()
		{
			if (!(Time.unscaledTime < nextValueTime))
			{
				nextValueTime = Time.unscaledTime + 0.1f;
				int num = Mathf.RoundToInt((Plugin.UseRollingAverage.Value && sampleCount > 0) ? (sampleSum / (float)sampleCount) : ReadSpeed());
				if (num != lastShownSpeed)
				{
					lastShownSpeed = num;
					speedVE.SetValue(num, true);
				}
			}
		}

		private static float ReadSpeed()
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			LocalPlayer i = LocalPlayer.I;
			if ((Object)(object)i == (Object)null)
			{
				return 0f;
			}
			Vector3 velocity = ((Player)i).Velocity;
			return ((Vector3)(ref velocity)).magnitude;
		}

		private static void Place()
		{
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: 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)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			if (speedVE != null && anchor != null && !(Time.unscaledTime < nextPlaceTime))
			{
				nextPlaceTime = Time.unscaledTime + 0.25f;
				Rect worldBound = anchor.worldBound;
				Rect worldBound2 = container.worldBound;
				if (!float.IsNaN(((Rect)(ref worldBound)).x) && !float.IsNaN(((Rect)(ref worldBound2)).x) && !(((Rect)(ref worldBound)).width <= 0f))
				{
					float num = 6f * ((float)Screen.height / 1080f);
					((VisualElement)speedVE).style.width = StyleLength.op_Implicit(((Rect)(ref worldBound)).width);
					((VisualElement)speedVE).style.height = StyleLength.op_Implicit(((Rect)(ref worldBound)).height);
					((VisualElement)speedVE).style.left = StyleLength.op_Implicit(((Rect)(ref worldBound)).xMin - ((Rect)(ref worldBound2)).xMin);
					((VisualElement)speedVE).style.top = StyleLength.op_Implicit(((Rect)(ref worldBound)).yMin - ((Rect)(ref worldBound2)).yMin + (((Rect)(ref worldBound)).height + num));
				}
			}
		}

		private static void Unwire()
		{
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			if (speedVE == null && (Object)(object)hud == (Object)null)
			{
				return;
			}
			if (oxygenAnchor != null)
			{
				oxygenAnchor.style.marginRight = StyleLength.op_Implicit((StyleKeyword)1);
			}
			if (anchor != null)
			{
				anchor.style.marginRight = StyleLength.op_Implicit((StyleKeyword)1);
				anchor.style.width = StyleLength.op_Implicit((StyleKeyword)1);
			}
			if (stackParent != null)
			{
				stackParent.style.alignSelf = StyleEnum<Align>.op_Implicit((StyleKeyword)1);
			}
			for (int i = 0; i < leftSpacers.Length; i++)
			{
				if (leftSpacers[i] != null)
				{
					leftSpacers[i].RemoveFromHierarchy();
				}
				leftSpacers[i] = null;
			}
			if (speedVE != null)
			{
				((VisualElement)speedVE).RemoveFromHierarchy();
			}
			speedVE = null;
			hud = null;
			container = null;
			stackParent = null;
			anchor = null;
			oxygenAnchor = null;
			usingFlexInsert = false;
			appliedGapPx = float.NaN;
			appliedEqualWidth = float.NaN;
			lastShownSpeed = int.MinValue;
			appliedCapacity = -1;
		}

		internal static string DumpTree()
		{
			StringBuilder stringBuilder = new StringBuilder("Speedometer TREE DUMP\n");
			if (container == null)
			{
				stringBuilder.AppendLine("not wired (HUD not found, or feature disabled). Enter a session and make sure the toggle is on, then retry.");
				return stringBuilder.ToString();
			}
			stringBuilder.AppendLine("-- LocalPlayerContainer (depth-limited) --");
			DumpChildren(stringBuilder, container, "  ", 0, 5);
			stringBuilder.AppendLine("-- our element --");
			Describe(stringBuilder, "speedVE", (VisualElement)(object)speedVE);
			if (speedVE != null)
			{
				DumpChildren(stringBuilder, (VisualElement)(object)speedVE, "  ", 0, 3);
			}
			stringBuilder.AppendLine("-- ResourcePanel ancestor chain (root last) --");
			DumpAncestors(stringBuilder, stackParent);
			return stringBuilder.ToString();
		}

		private static void DumpAncestors(StringBuilder sb, VisualElement ve)
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
			int num = 0;
			while (ve != null && num < 15)
			{
				IResolvedStyle resolvedStyle = ve.resolvedStyle;
				sb.AppendLine(string.Format("  ^{0}: <{1}> name='{2}' classes=[{3}] flexDir={4} justifyContent={5} alignItems={6} alignSelf={7} flexGrow={8:F1} flexShrink={9:F1} flexBasis={10} pos={11} left={12:F1} width={13:F1} worldBound={14}", num, ((object)ve).GetType().Name, ve.name, string.Join(",", ve.GetClasses()), resolvedStyle.flexDirection, resolvedStyle.justifyContent, resolvedStyle.alignItems, resolvedStyle.alignSelf, resolvedStyle.flexGrow, resolvedStyle.flexShrink, resolvedStyle.flexBasis, resolvedStyle.position, resolvedStyle.left, resolvedStyle.width, ve.worldBound));
				ve = ve.parent;
				num++;
			}
		}

		private static void Describe(StringBuilder sb, string label, VisualElement ve)
		{
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			if (ve == null)
			{
				sb.AppendLine(label + ": <null>");
				return;
			}
			IResolvedStyle resolvedStyle = ve.resolvedStyle;
			Label val = (Label)(object)((ve is Label) ? ve : null);
			string text = ((val != null) ? (" text='" + ((TextElement)val).text + "'") : string.Empty);
			sb.AppendLine(string.Format("{0}: <{1}> name='{2}' classes=[{3}] display={4} vis={5} opacity={6:F2} pos={7} left={8:F1} top={9:F1} marginRight={10:F1} marginBottom={11:F1} size={12:F1}x{13:F1} worldBound={14}{15}", label, ((object)ve).GetType().Name, ve.name, string.Join(",", ve.GetClasses()), resolvedStyle.display, resolvedStyle.visibility, resolvedStyle.opacity, resolvedStyle.position, resolvedStyle.left, resolvedStyle.top, resolvedStyle.marginRight, resolvedStyle.marginBottom, resolvedStyle.width, resolvedStyle.height, ve.worldBound, text));
		}

		private static void DumpChildren(StringBuilder sb, VisualElement ve, string indent, int depth, int maxDepth)
		{
			if (ve == null || depth > maxDepth)
			{
				return;
			}
			foreach (VisualElement item in ve.Children())
			{
				Describe(sb, indent + "-", item);
				DumpChildren(sb, item, indent + "  ", depth + 1, maxDepth);
			}
		}
	}
}