Decompiled source of Player Status Bars v0.2.1

BepInEx/plugins/PlayerStatusBars/PlayerStatusBars.dll

Decompiled 10 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using Microsoft.CodeAnalysis;
using TMPro;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace OtherPlayerStatusBars
{
	internal static class CadaverGrowthInfectionProvider
	{
		private sealed class SharedInfectionCache
		{
			private bool[] knownValues = Array.Empty<bool>();

			private bool[] infectionValues = Array.Empty<bool>();

			private float[] meters = Array.Empty<float>();

			private ulong[] actualClientIds = Array.Empty<ulong>();

			private int[] playerClientIds = Array.Empty<int>();

			private int validLength;

			public void EnsureCapacity(int length)
			{
				validLength = length;
				if (knownValues.Length < length)
				{
					int num = GrowCapacity(length);
					knownValues = new bool[num];
					infectionValues = new bool[num];
					meters = new float[num];
					actualClientIds = new ulong[num];
					playerClientIds = new int[num];
				}
			}

			public void Clear()
			{
				Array.Clear(knownValues, 0, validLength);
				Array.Clear(infectionValues, 0, validLength);
				Array.Clear(meters, 0, validLength);
				Array.Clear(actualClientIds, 0, validLength);
				Array.Clear(playerClientIds, 0, validLength);
				validLength = 0;
			}

			public void Set(int index, bool hasValue, float meter)
			{
				knownValues[index] = false;
				infectionValues[index] = hasValue;
				meters[index] = meter;
				actualClientIds[index] = ulong.MaxValue;
				playerClientIds[index] = -1;
			}

			public void Set(int index, PlayerControllerB player, bool hasValue, float meter)
			{
				knownValues[index] = true;
				infectionValues[index] = hasValue;
				meters[index] = meter;
				actualClientIds[index] = (((Object)(object)player != (Object)null) ? player.actualClientId : ulong.MaxValue);
				playerClientIds[index] = (((Object)(object)player != (Object)null) ? ((int)player.playerClientId) : (-1));
			}

			public bool TryGet(int index, PlayerControllerB player, out float meter)
			{
				bool hasInfection;
				return TryGet(index, player, out hasInfection, out meter) && hasInfection;
			}

			public bool TryGet(int index, PlayerControllerB player, out bool hasInfection, out float meter)
			{
				meter = 0f;
				hasInfection = false;
				if (index < 0 || index >= validLength || !knownValues[index])
				{
					return false;
				}
				if ((Object)(object)player == (Object)null || actualClientIds[index] != player.actualClientId || playerClientIds[index] != (int)player.playerClientId)
				{
					knownValues[index] = false;
					infectionValues[index] = false;
					return false;
				}
				hasInfection = infectionValues[index];
				meter = meters[index];
				return true;
			}

			private static int GrowCapacity(int requiredCapacity)
			{
				int num;
				for (num = 16; num < requiredCapacity; num *= 2)
				{
				}
				return num;
			}
		}

		private const float SampleInterval = 0.5f;

		private const float AbsentInstanceScanIntervalMin = 1f;

		private const float AbsentInstanceScanIntervalMax = 10f;

		private const float CachedInstanceMembershipCheckInterval = 2f;

		private const float InfectionMeterEpsilon = 0.0001f;

		private const int MinimumInfectionCacheCapacity = 16;

		private static readonly SharedInfectionCache sharedInfectionCache = new SharedInfectionCache();

		private static CadaverGrowthAI? cachedCadaverGrowthInstance;

		private static float nextInstanceScanTime;

		private static float nextSampleTime;

		private static float nextCachedInstanceMembershipCheckTime;

		private static float currentAbsentInstanceScanInterval = 1f;

		public static bool TryGetNormalizedInfection(PlayerControllerB player, out float infectionMeter)
		{
			return TryGetNormalizedInfection(player, -1, out infectionMeter);
		}

		public static bool TryGetNormalizedInfection(PlayerControllerB player, int knownPlayerSlot, out float infectionMeter)
		{
			bool hasInfection;
			return TryGetInfectionState(player, knownPlayerSlot, out hasInfection, out infectionMeter) && hasInfection;
		}

		public static bool TryGetInfectionState(PlayerControllerB player, int knownPlayerSlot, out bool hasInfection, out float infectionMeter)
		{
			infectionMeter = 0f;
			hasInfection = false;
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			RefreshCache();
			if (TryResolveInfectionIndex(player, knownPlayerSlot, out var infectionIndex))
			{
				return sharedInfectionCache.TryGet(infectionIndex, player, out hasInfection, out infectionMeter);
			}
			return false;
		}

		private static void RefreshCache()
		{
			if (Time.unscaledTime < nextSampleTime)
			{
				return;
			}
			nextSampleTime = Time.unscaledTime + 0.5f;
			PlayerInfection[] array = ResolveCadaverGrowthInstance()?.playerInfections;
			if (array == null)
			{
				sharedInfectionCache.Clear();
				return;
			}
			PlayerControllerB[] array2 = StartOfRound.Instance?.allPlayerScripts;
			if (array2 == null || array2.Length != array.Length)
			{
				sharedInfectionCache.Clear();
				return;
			}
			sharedInfectionCache.EnsureCapacity(array.Length);
			for (int i = 0; i < array.Length; i++)
			{
				PlayerControllerB val = array2[i];
				PlayerInfection val2 = array[i];
				if ((Object)(object)val == (Object)null || val2 == null)
				{
					sharedInfectionCache.Set(i, hasValue: false, 0f);
					continue;
				}
				float num = Mathf.Clamp01(val2.infectionMeter);
				if (!val2.infected || !(num > 0.0001f))
				{
					sharedInfectionCache.Set(i, val, hasValue: false, 0f);
				}
				else
				{
					sharedInfectionCache.Set(i, val, hasValue: true, num);
				}
			}
		}

		private static CadaverGrowthAI? ResolveCadaverGrowthInstance()
		{
			if ((Object)(object)cachedCadaverGrowthInstance != (Object)null)
			{
				if (IsCachedCadaverGrowthInstanceValid(cachedCadaverGrowthInstance))
				{
					return cachedCadaverGrowthInstance;
				}
				ClearCachedCadaverGrowthInstance();
			}
			if (Time.unscaledTime < nextInstanceScanTime)
			{
				return null;
			}
			nextInstanceScanTime = Time.unscaledTime + currentAbsentInstanceScanInterval;
			if ((Object)(object)RoundManager.Instance == (Object)null)
			{
				IncreaseAbsentInstanceScanBackoff();
				return null;
			}
			for (int i = 0; i < RoundManager.Instance.SpawnedEnemies.Count; i++)
			{
				EnemyAI obj = RoundManager.Instance.SpawnedEnemies[i];
				CadaverGrowthAI val = (CadaverGrowthAI)(object)((obj is CadaverGrowthAI) ? obj : null);
				if (val != null && IsCadaverGrowthInstanceUsable(val))
				{
					cachedCadaverGrowthInstance = val;
					ResetAbsentInstanceScanBackoff();
					return cachedCadaverGrowthInstance;
				}
			}
			IncreaseAbsentInstanceScanBackoff();
			return null;
		}

		private static bool IsCadaverGrowthInstanceValid(CadaverGrowthAI? cadaverGrowth)
		{
			if (IsCadaverGrowthInstanceUsable(cadaverGrowth))
			{
				return IsCadaverGrowthInSpawnedEnemies(cadaverGrowth);
			}
			return false;
		}

		private static bool IsCachedCadaverGrowthInstanceValid(CadaverGrowthAI? cadaverGrowth)
		{
			if (!IsCadaverGrowthInstanceUsable(cadaverGrowth))
			{
				return false;
			}
			if (Time.unscaledTime < nextCachedInstanceMembershipCheckTime)
			{
				return true;
			}
			nextCachedInstanceMembershipCheckTime = Time.unscaledTime + 2f;
			return IsCadaverGrowthInSpawnedEnemies(cadaverGrowth);
		}

		private static bool IsCadaverGrowthInstanceUsable(CadaverGrowthAI? cadaverGrowth)
		{
			if ((Object)(object)cadaverGrowth == (Object)null || ((EnemyAI)cadaverGrowth).isEnemyDead || !((Component)cadaverGrowth).gameObject.activeInHierarchy)
			{
				return false;
			}
			if ((Object)(object)StartOfRound.Instance == (Object)null || cadaverGrowth.playerInfections == null)
			{
				return false;
			}
			if (cadaverGrowth.playerInfections.Length != StartOfRound.Instance.allPlayerScripts.Length)
			{
				return false;
			}
			return true;
		}

		private static bool IsCadaverGrowthInSpawnedEnemies(CadaverGrowthAI? cadaverGrowth)
		{
			if ((Object)(object)RoundManager.Instance == (Object)null)
			{
				return false;
			}
			for (int i = 0; i < RoundManager.Instance.SpawnedEnemies.Count; i++)
			{
				if ((Object)(object)RoundManager.Instance.SpawnedEnemies[i] == (Object)(object)cadaverGrowth)
				{
					return true;
				}
			}
			return false;
		}

		private static void ClearCachedCadaverGrowthInstance()
		{
			cachedCadaverGrowthInstance = null;
			sharedInfectionCache.Clear();
			nextInstanceScanTime = 0f;
			nextCachedInstanceMembershipCheckTime = 0f;
		}

		internal static void InvalidateAll()
		{
			cachedCadaverGrowthInstance = null;
			sharedInfectionCache.Clear();
			nextInstanceScanTime = 0f;
			nextSampleTime = 0f;
			nextCachedInstanceMembershipCheckTime = 0f;
			currentAbsentInstanceScanInterval = 1f;
		}

		private static bool TryResolveInfectionIndex(PlayerControllerB player, int knownPlayerSlot, out int infectionIndex)
		{
			infectionIndex = -1;
			PlayerControllerB[] array = StartOfRound.Instance?.allPlayerScripts;
			if (array == null || array.Length == 0)
			{
				return false;
			}
			if ((uint)knownPlayerSlot < (uint)array.Length && (Object)(object)array[knownPlayerSlot] == (Object)(object)player)
			{
				infectionIndex = knownPlayerSlot;
				return true;
			}
			int num = (int)player.playerClientId;
			if ((uint)num < (uint)array.Length && (Object)(object)array[num] == (Object)(object)player)
			{
				infectionIndex = num;
				return true;
			}
			for (int i = 0; i < array.Length; i++)
			{
				if ((Object)(object)array[i] == (Object)(object)player)
				{
					infectionIndex = i;
					return true;
				}
			}
			return false;
		}

		private static void IncreaseAbsentInstanceScanBackoff()
		{
			currentAbsentInstanceScanInterval = Mathf.Min(currentAbsentInstanceScanInterval * 2f, 10f);
		}

		private static void ResetAbsentInstanceScanBackoff()
		{
			currentAbsentInstanceScanInterval = 1f;
		}
	}
	internal static class MyPluginInfo
	{
		public const string PluginGuid = "playerstatusbars";

		public const string PluginName = "PlayerStatusBars";

		public const string PluginVersion = "0.2.1";
	}
	internal sealed class PlayerStatusBarManager : MonoBehaviour
	{
		private const float DebugSummaryInterval = 5f;

		private const float ActiveRefreshInterval = 0.5f;

		private const float IdleRefreshInterval = 2.5f;

		private const int ScanSlotsPerRefresh = 4;

		private const int MinimumPlayerSlotCapacity = 16;

		private PlayerStatusBarView?[] trackedBarsBySlot = Array.Empty<PlayerStatusBarView>();

		private PlayerStatusBarView?[] activeBars = Array.Empty<PlayerStatusBarView>();

		private int activeBarCount;

		private int activePlayerSlotCount;

		private readonly Stack<PlayerStatusBarView> pooledBars = new Stack<PlayerStatusBarView>();

		private float nextRefreshTime;

		private float nextDebugSummaryTime;

		private int refreshScanCursor;

		private void Start()
		{
			Plugin.LogDebug($"Runtime manager started active={((Component)this).gameObject.activeInHierarchy}.");
		}

		private void Update()
		{
			StatusBarConfig settings = Plugin.Settings;
			if (!settings.Enabled)
			{
				if (activeBarCount != 0 || !(Time.unscaledTime < nextRefreshTime))
				{
					LogDebugBlocked("disabled");
					ClearBars();
					PlayerStatusSnapshotSync.ClearSnapshots();
					CadaverGrowthInfectionProvider.InvalidateAll();
					ScheduleNextRefresh(hasActiveBars: false);
				}
				return;
			}
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : instance?.localPlayerController);
			PlayerControllerB[] array = instance?.allPlayerScripts;
			if ((Object)(object)instance == (Object)null || (Object)(object)val == (Object)null || array == null)
			{
				if (Time.unscaledTime >= nextRefreshTime)
				{
					ScheduleNextRefresh(hasActiveBars: false);
					LogDebugBlocked($"not-ready startOfRound={(Object)(object)instance != (Object)null} localPlayer={(Object)(object)val != (Object)null} allPlayers={array != null}");
					ClearBars();
					PlayerStatusSnapshotSync.ClearSnapshots();
					CadaverGrowthInfectionProvider.InvalidateAll();
				}
				return;
			}
			EnsureSlotCapacity(array.Length);
			bool flag = !ShouldSkipBarsForShipState(instance);
			if (!flag)
			{
				if (Time.unscaledTime >= nextRefreshTime)
				{
					LogDebugBlocked($"orbit-hidden inShipPhase={instance.inShipPhase} shipHasLanded={instance.shipHasLanded} shipDoorsEnabled={instance.shipDoorsEnabled} shipIsLeaving={instance.shipIsLeaving} dayStarted={TimeOfDay.Instance?.currentDayTimeStarted}");
				}
				ClearBars();
				PlayerStatusSnapshotSync.ClearSnapshots();
				CadaverGrowthInfectionProvider.InvalidateAll();
				ScheduleNextRefresh(hasActiveBars: false);
				return;
			}
			PlayerStatusSnapshotSync.Tick(instance, array);
			if (activeBarCount != 0 || !(Time.unscaledTime < nextRefreshTime))
			{
				if (Time.unscaledTime >= nextRefreshTime)
				{
					RefreshPlayerSlice(instance, val, array);
				}
				if (activeBarCount != 0)
				{
					PlayerStatusFrameContext frameContext = CreateFrameContext(settings, val, flag);
					TickTrackedBars(in frameContext);
				}
			}
		}

		private void RefreshPlayerSlice(StartOfRound startOfRound, PlayerControllerB localPlayer, PlayerControllerB[] allPlayers)
		{
			bool flag = Plugin.Settings.DebugLogging && Time.unscaledTime >= nextDebugSummaryTime;
			int num = 0;
			int num2 = activeBarCount;
			int num3 = 0;
			if (allPlayers.Length == 0)
			{
				ClearBars();
				LogDebugSummary(startOfRound, localPlayer, 0, num, num2, num3);
				ScheduleNextRefresh(num > 0 || num2 > 0 || allPlayers.Length > 4);
				return;
			}
			int num4 = Mathf.Min(4, allPlayers.Length);
			for (int i = 0; i < num4; i++)
			{
				int num5 = refreshScanCursor;
				refreshScanCursor = (refreshScanCursor + 1) % allPlayers.Length;
				PlayerControllerB val = allPlayers[num5];
				if (!ShouldTrackPlayer(startOfRound, val, localPlayer, num5, allPlayers, out string skipReason))
				{
					num3++;
					if (flag)
					{
						LogDebugPlayerSkip(num5, val, skipReason);
					}
					RemoveBar(num5);
					continue;
				}
				int num6 = ResolvePlayerKey(val, num5, allPlayers);
				PlayerStatusBarView playerStatusBarView = trackedBarsBySlot[num5];
				if (!((Object)(object)playerStatusBarView != (Object)null) || !playerStatusBarView.MatchesPlayer(val, localPlayer, num6))
				{
					if ((Object)(object)playerStatusBarView != (Object)null)
					{
						RemoveBar(num5);
					}
					PlayerStatusBarView playerStatusBarView2 = RentBarView(num6, val);
					if ((Object)(object)playerStatusBarView2 == (Object)null)
					{
						Plugin.Log.LogWarning((object)$"Failed to create player status bar for playerKey={num6}.");
						continue;
					}
					trackedBarsBySlot[num5] = playerStatusBarView2;
					AddActiveBar(playerStatusBarView2);
					num++;
					Plugin.LogDebug($"Created bar playerKey={num6} slot={num5} clientId={val.playerClientId} actualClientId={val.actualClientId} name='{val.playerUsername}' health={val.health} active={((Component)val).gameObject.activeInHierarchy}.");
				}
			}
			LogDebugSummary(startOfRound, localPlayer, allPlayers.Length, num, num2, num3);
			ScheduleNextRefresh(num > 0 || num2 > 0 || allPlayers.Length > 4);
		}

		private void EnsureSlotCapacity(int slotCount)
		{
			if (slotCount < activePlayerSlotCount)
			{
				ReleaseBarsFrom(slotCount);
			}
			activePlayerSlotCount = slotCount;
			if (slotCount == 0 || refreshScanCursor >= slotCount)
			{
				refreshScanCursor = 0;
			}
			if (trackedBarsBySlot.Length < slotCount)
			{
				int num = trackedBarsBySlot.Length;
				PlayerStatusBarView[] destinationArray = new PlayerStatusBarView[GrowSlotCapacity(slotCount)];
				if (num > 0)
				{
					Array.Copy(trackedBarsBySlot, destinationArray, num);
				}
				trackedBarsBySlot = destinationArray;
			}
		}

		private static int GrowSlotCapacity(int requiredCapacity)
		{
			int num;
			for (num = 16; num < requiredCapacity; num *= 2)
			{
			}
			return num;
		}

		private void ScheduleNextRefresh(bool hasActiveBars)
		{
			nextRefreshTime = Time.unscaledTime + (hasActiveBars ? 0.5f : 2.5f);
		}

		private PlayerStatusBarView? RentBarView(int playerKey, PlayerControllerB player)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Expected O, but got Unknown
			PlayerStatusBarView playerStatusBarView;
			if (pooledBars.Count > 0)
			{
				playerStatusBarView = pooledBars.Pop();
				((Component)playerStatusBarView).transform.SetParent(((Component)this).transform, false);
				((Object)playerStatusBarView).name = $"OtherPlayerStatusBar_{playerKey}";
				playerStatusBarView.Bind(player, playerKey);
				return playerStatusBarView;
			}
			GameObject val = new GameObject($"OtherPlayerStatusBar_{playerKey}");
			val.transform.SetParent(((Component)this).transform, false);
			playerStatusBarView = val.AddComponent<PlayerStatusBarView>();
			if (playerStatusBarView.Initialize(player, playerKey))
			{
				return playerStatusBarView;
			}
			Object.Destroy((Object)(object)val);
			return null;
		}

		private void ReleaseBarView(PlayerStatusBarView view)
		{
			view.Release();
			((Component)view).transform.SetParent(((Component)this).transform, false);
			pooledBars.Push(view);
		}

		private void TickTrackedBars(in PlayerStatusFrameContext frameContext)
		{
			for (int i = 0; i < activeBarCount; i++)
			{
				PlayerStatusBarView playerStatusBarView = activeBars[i];
				if ((Object)(object)playerStatusBarView != (Object)null)
				{
					playerStatusBarView.Tick(in frameContext);
				}
			}
		}

		private void AddActiveBar(PlayerStatusBarView view)
		{
			EnsureActiveBarCapacity(activeBarCount + 1);
			activeBars[activeBarCount] = view;
			activeBarCount++;
		}

		private void RemoveActiveBar(PlayerStatusBarView view)
		{
			for (int i = 0; i < activeBarCount; i++)
			{
				if (!((Object)(object)activeBars[i] != (Object)(object)view))
				{
					int num = activeBarCount - 1;
					activeBars[i] = activeBars[num];
					activeBars[num] = null;
					activeBarCount = num;
					break;
				}
			}
		}

		private void EnsureActiveBarCapacity(int requiredCapacity)
		{
			if (activeBars.Length < requiredCapacity)
			{
				PlayerStatusBarView[] destinationArray = new PlayerStatusBarView[GrowSlotCapacity(requiredCapacity)];
				if (activeBarCount > 0)
				{
					Array.Copy(activeBars, destinationArray, activeBarCount);
				}
				activeBars = destinationArray;
			}
		}

		private static PlayerStatusFrameContext CreateFrameContext(StatusBarConfig settings, PlayerControllerB localPlayer, bool canShowGroup)
		{
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: 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_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_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			Camera viewCamera = null;
			bool hasBillboardRotation = false;
			Quaternion rotation = Quaternion.identity;
			Vector3 observerPosition = ((Component)localPlayer).transform.position;
			if (canShowGroup)
			{
				viewCamera = StatusBarBillboard.ResolveViewCamera();
				hasBillboardRotation = StatusBarBillboard.TryResolveBillboardRotation(viewCamera, out rotation);
				observerPosition = ResolveObserverPosition(localPlayer, viewCamera);
			}
			return new PlayerStatusFrameContext(settings, localPlayer, viewCamera, hasBillboardRotation, rotation, canShowGroup, observerPosition);
		}

		private static Vector3 ResolveObserverPosition(PlayerControllerB localPlayer, Camera? viewCamera)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: 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 ((Object)(object)viewCamera != (Object)null)
			{
				return ((Component)viewCamera).transform.position;
			}
			PlayerControllerB spectatedPlayerScript = localPlayer.spectatedPlayerScript;
			if (localPlayer.isPlayerDead && (Object)(object)spectatedPlayerScript != (Object)null && !spectatedPlayerScript.isPlayerDead)
			{
				return ((Component)spectatedPlayerScript).transform.position;
			}
			return ((Component)localPlayer).transform.position;
		}

		private static bool ShouldTrackPlayer(StartOfRound startOfRound, PlayerControllerB player, PlayerControllerB localPlayer, int slot, PlayerControllerB[] allPlayers, out string skipReason)
		{
			if ((Object)(object)player == (Object)null || (Object)(object)localPlayer == (Object)null)
			{
				skipReason = "null-player-or-local";
				return false;
			}
			if ((Object)(object)player == (Object)(object)localPlayer)
			{
				skipReason = "local-player";
				return false;
			}
			if (!((Component)player).gameObject.activeInHierarchy)
			{
				skipReason = "inactive-gameobject";
				return false;
			}
			int playerKey = ResolvePlayerKey(player, slot, allPlayers);
			if (!IsConnectedPlayerSlot(startOfRound, player, playerKey))
			{
				skipReason = "not-connected";
				return false;
			}
			skipReason = string.Empty;
			return true;
		}

		private static bool IsConnectedPlayerSlot(StartOfRound startOfRound, PlayerControllerB player, int playerKey)
		{
			if (startOfRound.ClientPlayerList.TryGetValue(player.actualClientId, out var value))
			{
				return value == playerKey;
			}
			if (player.isPlayerControlled)
			{
				return !player.disconnectedMidGame;
			}
			return false;
		}

		private static int ResolvePlayerKey(PlayerControllerB player, int slotIndex, PlayerControllerB[] allPlayers)
		{
			int num = (int)player.playerClientId;
			if (num >= 0 && num < allPlayers.Length && (Object)(object)allPlayers[num] == (Object)(object)player)
			{
				return num;
			}
			return slotIndex;
		}

		internal static bool ShouldSkipBarsForShipState(StartOfRound startOfRound)
		{
			if (!Plugin.Settings.HideInOrbit)
			{
				return false;
			}
			if (startOfRound.shipIsLeaving)
			{
				return true;
			}
			if ((Object)(object)TimeOfDay.Instance != (Object)null && TimeOfDay.Instance.currentDayTimeStarted)
			{
				return false;
			}
			if (startOfRound.inShipPhase)
			{
				return !startOfRound.shipHasLanded;
			}
			return false;
		}

		private void RemoveBar(int playerId)
		{
			if (playerId < 0 || playerId >= trackedBarsBySlot.Length)
			{
				return;
			}
			PlayerStatusBarView playerStatusBarView = trackedBarsBySlot[playerId];
			if (!((Object)(object)playerStatusBarView == (Object)null))
			{
				trackedBarsBySlot[playerId] = null;
				RemoveActiveBar(playerStatusBarView);
				if ((Object)(object)playerStatusBarView != (Object)null)
				{
					ReleaseBarView(playerStatusBarView);
				}
				Plugin.LogDebug($"Removed bar playerKey={playerId}.");
			}
		}

		private void ClearBars()
		{
			int num = activeBarCount;
			for (int i = 0; i < trackedBarsBySlot.Length; i++)
			{
				PlayerStatusBarView playerStatusBarView = trackedBarsBySlot[i];
				if ((Object)(object)playerStatusBarView != (Object)null)
				{
					trackedBarsBySlot[i] = null;
					ReleaseBarView(playerStatusBarView);
				}
			}
			Array.Clear(activeBars, 0, activeBarCount);
			activeBarCount = 0;
			if (num > 0)
			{
				CadaverGrowthInfectionProvider.InvalidateAll();
				Plugin.LogDebug($"Cleared all bars count={num}.");
			}
		}

		private void ReleaseBarsFrom(int firstSlot)
		{
			for (int i = firstSlot; i < trackedBarsBySlot.Length; i++)
			{
				RemoveBar(i);
			}
		}

		private void OnDestroy()
		{
			Plugin.LogDebug($"Runtime manager destroyed tracked={activeBarCount}.");
			PlayerStatusSnapshotSync.ClearSnapshots();
			CadaverGrowthInfectionProvider.InvalidateAll();
			DestroyAllBars();
		}

		private void DestroyAllBars()
		{
			for (int i = 0; i < trackedBarsBySlot.Length; i++)
			{
				PlayerStatusBarView playerStatusBarView = trackedBarsBySlot[i];
				if ((Object)(object)playerStatusBarView != (Object)null)
				{
					Object.Destroy((Object)(object)((Component)playerStatusBarView).gameObject);
				}
			}
			trackedBarsBySlot = Array.Empty<PlayerStatusBarView>();
			activeBars = Array.Empty<PlayerStatusBarView>();
			activeBarCount = 0;
			while (pooledBars.Count > 0)
			{
				PlayerStatusBarView playerStatusBarView2 = pooledBars.Pop();
				if ((Object)(object)playerStatusBarView2 != (Object)null)
				{
					Object.Destroy((Object)(object)((Component)playerStatusBarView2).gameObject);
				}
			}
		}

		private void OnEnable()
		{
			Plugin.LogDebug("Runtime manager enabled.");
			Plugin.Settings.SettingsChanged += HandleSettingsChanged;
		}

		private void OnDisable()
		{
			Plugin.LogDebug($"Runtime manager disabled tracked={activeBarCount}.");
			Plugin.Settings.SettingsChanged -= HandleSettingsChanged;
		}

		private void HandleSettingsChanged()
		{
			nextRefreshTime = 0f;
		}

		private void LogDebugBlocked(string reason)
		{
			if (Plugin.Settings.DebugLogging && !(Time.unscaledTime < nextDebugSummaryTime))
			{
				nextDebugSummaryTime = Time.unscaledTime + 5f;
				Plugin.LogDebug($"Refresh blocked reason={reason} tracked={activeBarCount}.");
			}
		}

		private static void LogDebugPlayerSkip(int slot, PlayerControllerB player, string reason)
		{
			if (Plugin.Settings.DebugLogging && !(reason == "local-player"))
			{
				Plugin.LogDebug(string.Format("Skipped slot={0} reason={1} clientId={2} name='{3}' controlled={4} dead={5} health={6} active={7}.", slot, reason, ((Object)(object)player != (Object)null) ? player.playerClientId.ToString() : "null", ((Object)(object)player != (Object)null) ? player.playerUsername : "null", (Object)(object)player != (Object)null && player.isPlayerControlled, (Object)(object)player != (Object)null && player.isPlayerDead, ((Object)(object)player != (Object)null) ? player.health : (-1), (Object)(object)player != (Object)null && ((Component)player).gameObject.activeInHierarchy));
			}
		}

		private void LogDebugSummary(StartOfRound startOfRound, PlayerControllerB localPlayer, int playerSlots, int createdCount, int existingCount, int skippedCount)
		{
			if (Plugin.Settings.DebugLogging && !(Time.unscaledTime < nextDebugSummaryTime))
			{
				nextDebugSummaryTime = Time.unscaledTime + 5f;
				Camera val = StatusBarBillboard.ResolveViewCamera();
				Plugin.LogDebug(string.Format("Refresh summary slots={0} tracked={1} created={2} existing={3} skipped={4} localClient={5} inShipPhase={6} shipHasLanded={7} shipDoorsEnabled={8} shipIsLeaving={9} dayStarted={10} camera='{11}'.", playerSlots, activeBarCount, createdCount, existingCount, skippedCount, localPlayer.playerClientId, startOfRound.inShipPhase, startOfRound.shipHasLanded, startOfRound.shipDoorsEnabled, startOfRound.shipIsLeaving, TimeOfDay.Instance?.currentDayTimeStarted, ((Object)(object)val != (Object)null) ? ((Object)val).name : "none"));
			}
		}
	}
	internal sealed class PlayerStatusBarStrip : MonoBehaviour
	{
		internal enum StripType
		{
			Health,
			Infection
		}

		private const float Width = 172f;

		private const float Height = 20f;

		private bool visible = true;

		private string label = string.Empty;

		private float currentValue;

		private float maxValue = 1f;

		private bool renderAsPercent;

		private bool labelOnly;

		private StripType stripType;

		private RectTransform rectTransform;

		private RectTransform fillRect;

		private Image backgroundImage;

		private Image fillImage;

		private TextMeshProUGUI text;

		private Outline border;

		private bool dirty = true;

		private bool lastAppliedVisible = true;

		private float lastAppliedNormalized = -1f;

		private string lastAppliedText = string.Empty;

		private bool lastAppliedTextEnabled = true;

		private Color lastAppliedBackgroundColor = Color.clear;

		private Color lastAppliedFillColor = Color.clear;

		private Color lastAppliedBorderColor = Color.clear;

		private int lastAppliedSettingsRevision = -1;

		private bool hasFillColorOverride;

		private Color fillColorOverride;

		public void Initialize(StripType type)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: 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_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Expected O, but got Unknown
			//IL_00f0: 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)
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_0163: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Expected O, but got Unknown
			//IL_018d: Unknown result type (might be due to invalid IL or missing references)
			//IL_019d: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_024e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0254: Expected O, but got Unknown
			//IL_026d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0278: Unknown result type (might be due to invalid IL or missing references)
			//IL_028d: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_02e9: Unknown result type (might be due to invalid IL or missing references)
			stripType = type;
			rectTransform = ((Component)this).gameObject.GetComponent<RectTransform>();
			if ((Object)(object)rectTransform == (Object)null)
			{
				rectTransform = ((Component)this).gameObject.AddComponent<RectTransform>();
			}
			rectTransform.sizeDelta = new Vector2(172f, 20f);
			rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
			rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
			rectTransform.pivot = new Vector2(0.5f, 0.5f);
			GameObject val = new GameObject("Background", new Type[3]
			{
				typeof(RectTransform),
				typeof(Image),
				typeof(Outline)
			});
			val.transform.SetParent(((Component)this).transform, false);
			RectTransform component = val.GetComponent<RectTransform>();
			component.anchorMin = Vector2.zero;
			component.anchorMax = Vector2.one;
			component.offsetMin = Vector2.zero;
			component.offsetMax = Vector2.zero;
			backgroundImage = val.GetComponent<Image>();
			((Graphic)backgroundImage).raycastTarget = false;
			border = val.GetComponent<Outline>();
			GameObject val2 = new GameObject("Fill", new Type[2]
			{
				typeof(RectTransform),
				typeof(Image)
			});
			val2.transform.SetParent(val.transform, false);
			fillRect = val2.GetComponent<RectTransform>();
			fillRect.anchorMin = Vector2.zero;
			fillRect.anchorMax = Vector2.one;
			fillRect.pivot = new Vector2(0f, 0.5f);
			fillRect.offsetMin = new Vector2(1f, 1f);
			fillRect.offsetMax = new Vector2(-1f, -1f);
			((Transform)fillRect).localScale = Vector3.one;
			fillImage = val2.GetComponent<Image>();
			((Graphic)fillImage).raycastTarget = false;
			fillImage.type = (Type)0;
			GameObject val3 = new GameObject("Text", new Type[2]
			{
				typeof(RectTransform),
				typeof(TextMeshProUGUI)
			});
			val3.transform.SetParent(val.transform, false);
			RectTransform component2 = val3.GetComponent<RectTransform>();
			component2.anchorMin = Vector2.zero;
			component2.anchorMax = Vector2.one;
			component2.offsetMin = new Vector2(6f, 1f);
			component2.offsetMax = new Vector2(-6f, -1f);
			text = val3.GetComponent<TextMeshProUGUI>();
			((TMP_Text)text).alignment = (TextAlignmentOptions)514;
			((TMP_Text)text).fontSize = 12f;
			((TMP_Text)text).enableWordWrapping = false;
			((Graphic)text).color = Color.white;
			((Graphic)text).raycastTarget = false;
		}

		public void SetDisplay(string displayLabel, float value, float max, bool showPercent)
		{
			float num = Mathf.Clamp(value, 0f, (max <= 0f) ? 1f : max);
			float num2 = Mathf.Max(max, 0.0001f);
			if (label == displayLabel && Mathf.Approximately(currentValue, num) && Mathf.Approximately(maxValue, num2) && renderAsPercent == showPercent && !labelOnly)
			{
				visible = true;
				return;
			}
			label = displayLabel;
			currentValue = num;
			maxValue = num2;
			renderAsPercent = showPercent;
			labelOnly = false;
			visible = true;
			dirty = true;
		}

		public void SetLabelOnly(string displayLabel, float fillValue, float max)
		{
			float num = Mathf.Clamp(fillValue, 0f, (max <= 0f) ? 1f : max);
			float num2 = Mathf.Max(max, 0.0001f);
			if (label == displayLabel && Mathf.Approximately(currentValue, num) && Mathf.Approximately(maxValue, num2) && labelOnly)
			{
				visible = true;
				return;
			}
			label = displayLabel;
			currentValue = num;
			maxValue = num2;
			renderAsPercent = false;
			labelOnly = true;
			visible = true;
			dirty = true;
		}

		public void SetVisible(bool isVisible)
		{
			if (visible != isVisible || ((Component)this).gameObject.activeSelf != isVisible)
			{
				visible = isVisible;
				((Component)this).gameObject.SetActive(isVisible);
				dirty = true;
			}
		}

		public void SetFillColorOverride(bool enabled, Color color)
		{
			//IL_0023: 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)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			if (hasFillColorOverride != enabled || (enabled && !(fillColorOverride == color)))
			{
				hasFillColorOverride = enabled;
				fillColorOverride = color;
				dirty = true;
			}
		}

		public void ApplyIfDirty(StatusBarConfig settings)
		{
			//IL_006f: 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_0074: 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_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_010b: Unknown result type (might be due to invalid IL or missing references)
			//IL_010d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: 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_0193: Unknown result type (might be due to invalid IL or missing references)
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01da: Unknown result type (might be due to invalid IL or missing references)
			//IL_01db: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fc: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)backgroundImage == (Object)null || (Object)(object)fillImage == (Object)null || (Object)(object)this.text == (Object)null || !visible || (!dirty && settings.Revision == lastAppliedSettingsRevision))
			{
				return;
			}
			float num = Mathf.Clamp01(currentValue / maxValue);
			Color val = ((stripType == StripType.Health) ? settings.GetHealthFillColor() : settings.GetInfectionFillColor());
			if (hasFillColorOverride)
			{
				val = fillColorOverride;
			}
			Color backgroundColor = settings.GetBackgroundColor();
			Color borderColor = settings.GetBorderColor();
			bool flag = ((stripType == StripType.Health) ? settings.ShowHealthText : settings.ShowInfectionText);
			string text = string.Empty;
			if (flag)
			{
				text = (labelOnly ? label : (renderAsPercent ? StatusBarTextCache.GetInfectionLabel(Mathf.RoundToInt(num * 100f)) : StatusBarTextCache.GetHealthLabel(Mathf.RoundToInt(currentValue), Mathf.RoundToInt(maxValue))));
			}
			bool flag2 = backgroundColor != lastAppliedBackgroundColor || val != lastAppliedFillColor || borderColor != lastAppliedBorderColor;
			bool flag3 = !Mathf.Approximately(num, lastAppliedNormalized);
			bool flag4 = flag != lastAppliedTextEnabled;
			bool flag5 = text != lastAppliedText;
			bool flag6 = visible != lastAppliedVisible;
			if (dirty || flag2 || flag3 || flag4 || flag5 || flag6)
			{
				if (flag2)
				{
					((Graphic)backgroundImage).color = backgroundColor;
					((Shadow)border).effectColor = borderColor;
					((Shadow)border).effectDistance = new Vector2(1f, -1f);
					((Graphic)fillImage).color = val;
					lastAppliedBackgroundColor = backgroundColor;
					lastAppliedFillColor = val;
					lastAppliedBorderColor = borderColor;
				}
				if (dirty || flag3)
				{
					((Transform)fillRect).localScale = new Vector3(num, 1f, 1f);
					lastAppliedNormalized = num;
				}
				if (dirty || flag4)
				{
					((Behaviour)this.text).enabled = flag;
					lastAppliedTextEnabled = flag;
				}
				if (flag && (dirty || flag5))
				{
					((TMP_Text)this.text).text = text;
					lastAppliedText = text;
				}
				lastAppliedVisible = visible;
				lastAppliedSettingsRevision = settings.Revision;
				dirty = false;
			}
		}
	}
	internal sealed class PlayerStatusBarView : MonoBehaviour
	{
		private static readonly Color CriticalHealthBarColor = new Color(0.88f, 0.15f, 0.12f, 0.95f);

		private const int LowHealthRegenerationTarget = 20;

		private const int InferredCriticalHealth = 5;

		private const float LowHealthRegenerationInterval = 1f;

		private const float HealthDisplayStepInterval = 0.05f;

		private const int InfectionDisplayMinimumPercent = 1;

		private const int InfectionDisplayMaximumPercent = 99;

		private const float InfectionPredictionWindow = 0.5f;

		private const float InfectionDisplayStepInterval = 0.1f;

		private PlayerControllerB targetPlayer;

		private Transform? anchor;

		private ulong boundActualClientId = ulong.MaxValue;

		private int boundPlayerKey = -1;

		private PlayerStatusBarStrip healthStrip;

		private PlayerStatusBarStrip infectionStrip;

		private RectTransform healthStripRect;

		private RectTransform infectionStripRect;

		private Canvas worldCanvas;

		private RectTransform canvasRect;

		private int lastDisplayedHealth = int.MinValue;

		private int lastDisplayedInfectionPercent = int.MinValue;

		private bool wasHealthVisible;

		private bool wasInfectionVisible;

		private string lastDisplayedHealthLabel = string.Empty;

		private bool lastCanvasEnabled = true;

		private Camera? lastAppliedCanvasCamera;

		private string lastDebugVisibilityState = string.Empty;

		private float nextDebugVisibilityLogTime;

		private float lastAppliedUiScale = -1f;

		private float lastAppliedHealthBarYOffset = float.NaN;

		private float lastAppliedInfectionBarYOffset = float.NaN;

		private bool isLowHealthPredictionActive;

		private bool wasTargetDead;

		private bool hasObservedRawHealth;

		private bool hasObservedDamageTimestamp;

		private bool hasObservedRawCriticalState;

		private bool lastObservedRawCriticalState;

		private bool holdNaturalRecoveredLowHealthDisplay;

		private bool isInfectionPredictionActive;

		private bool hasDisplayedHealth;

		private bool hasDisplayedInfectionPercent;

		private bool wasUsingSnapshot;

		private bool lastDebugRawCriticalState;

		private bool lastDebugDamageSignalChanged;

		private bool lastDebugCriticalSignalChanged;

		private int lastObservedRawHealth = int.MinValue;

		private int predictionObservedRawHealth = int.MinValue;

		private int predictionStartHealth = int.MinValue;

		private int predictedLowHealth = int.MinValue;

		private int displayedHealth = int.MinValue;

		private int healthDisplayTarget = int.MinValue;

		private int naturalRecoveredRawHealth = int.MinValue;

		private int displayedInfectionPercent = int.MinValue;

		private int lastDebugRawHealth = int.MinValue;

		private int lastDebugDisplayHealthTarget = int.MinValue;

		private int lastDebugDisplayedHealth = int.MinValue;

		private float lowHealthPredictionStartTime;

		private float lastObservedDamageTimestamp;

		private float lastObservedInfectionMeter;

		private float lastObservedInfectionSampleTime;

		private float infectionPredictionStartMeter;

		private float infectionPredictionStartTime;

		private float infectionPredictionRate;

		private float nextHealthDisplayStepTime;

		private float nextInfectionDisplayStepTime;

		public int PlayerId
		{
			get
			{
				if (boundPlayerKey < 0)
				{
					if (!((Object)(object)targetPlayer != (Object)null))
					{
						return -1;
					}
					return (int)targetPlayer.playerClientId;
				}
				return boundPlayerKey;
			}
		}

		public bool Initialize(PlayerControllerB player, int playerKey)
		{
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			worldCanvas = ((Component)this).gameObject.AddComponent<Canvas>();
			worldCanvas.renderMode = (RenderMode)2;
			worldCanvas.overrideSorting = true;
			worldCanvas.sortingOrder = 50;
			canvasRect = ((Component)worldCanvas).GetComponent<RectTransform>();
			canvasRect.sizeDelta = new Vector2(220f, 84f);
			if (!TryCreateStrip("HealthStrip", out healthStrip))
			{
				return false;
			}
			healthStripRect = ((Component)healthStrip).GetComponent<RectTransform>();
			if (!TryCreateStrip("InfectionStrip", out infectionStrip))
			{
				return false;
			}
			infectionStripRect = ((Component)infectionStrip).GetComponent<RectTransform>();
			Bind(player, playerKey);
			return true;
		}

		internal void Bind(PlayerControllerB player, int playerKey)
		{
			targetPlayer = player;
			boundPlayerKey = playerKey;
			boundActualClientId = player.actualClientId;
			anchor = ResolveAnchor(player);
			ResetRuntimeState();
			((Object)this).name = $"PlayerStatusBar_{player.playerClientId}";
			((Component)this).gameObject.SetActive(true);
		}

		internal void Release()
		{
			if ((Object)(object)worldCanvas != (Object)null)
			{
				SetCanvasEnabled(enabled: false);
			}
			if ((Object)(object)healthStrip != (Object)null)
			{
				healthStrip.SetVisible(isVisible: false);
			}
			if ((Object)(object)infectionStrip != (Object)null)
			{
				infectionStrip.SetVisible(isVisible: false);
			}
			targetPlayer = null;
			anchor = null;
			boundPlayerKey = -1;
			boundActualClientId = ulong.MaxValue;
			ResetRuntimeState();
			((Component)this).gameObject.SetActive(false);
		}

		public bool IsStillValid(PlayerControllerB localPlayer)
		{
			if ((Object)(object)targetPlayer == (Object)null || (Object)(object)localPlayer == (Object)null)
			{
				return false;
			}
			if ((Object)(object)targetPlayer == (Object)(object)localPlayer)
			{
				return false;
			}
			if (!targetPlayer.disconnectedMidGame)
			{
				return ((Component)targetPlayer).gameObject.activeInHierarchy;
			}
			return false;
		}

		public bool MatchesPlayer(PlayerControllerB player, PlayerControllerB localPlayer, int playerKey)
		{
			if (IsStillValid(localPlayer) && (Object)(object)targetPlayer == (Object)(object)player && boundPlayerKey == playerKey)
			{
				return boundActualClientId == player.actualClientId;
			}
			return false;
		}

		internal void Tick(in PlayerStatusFrameContext context)
		{
			//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ff: 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_010c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			//IL_0125: Unknown result type (might be due to invalid IL or missing references)
			//IL_025f: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)targetPlayer == (Object)null)
			{
				return;
			}
			StatusBarConfig settings = context.Settings;
			if (!context.CanShowGroup)
			{
				HideAll(settings, "group-hidden", context.ViewCamera, -1f);
				return;
			}
			PlayerStatusSnapshot snapshot;
			bool flag = PlayerStatusSnapshotSync.TryGetSnapshot(targetPlayer, out snapshot);
			HandleSnapshotSourceTransition(flag);
			bool targetIsDead = ((!flag) ? (targetPlayer.isPlayerDead || targetPlayer.health <= 0) : (snapshot.IsDead || snapshot.Health <= 0));
			HandleDeathStateTransition(targetIsDead);
			if (!IsTargetReadyForDisplay(targetIsDead))
			{
				HideAll(settings, "target-not-ready", context.ViewCamera, -1f);
				return;
			}
			if (anchor == null)
			{
				anchor = ResolveAnchor(targetPlayer);
			}
			Vector3 val = (((Object)(object)anchor != (Object)null) ? anchor.position : ((Component)targetPlayer).transform.position);
			((Component)this).transform.position = val + Vector3.up * context.AnchorYOffset;
			if (context.HasBillboardRotation)
			{
				SetBillboardRotation(context.BillboardRotation);
			}
			ApplyCanvasCamera(context.ViewCamera);
			ApplyCanvasScale(context.UiScale);
			float distanceSqr;
			bool flag2 = ShouldShowForDistance(in context, out distanceSqr);
			if (!flag2)
			{
				HideForDistance(context.ViewCamera, distanceSqr);
				return;
			}
			int rawHealth = (flag ? Mathf.Clamp(snapshot.Health, 0, 100) : Mathf.Clamp(targetPlayer.health, 0, 100));
			bool rawCriticalState = (flag ? snapshot.IsCritical : (targetPlayer.criticallyInjured || targetPlayer.bleedingHeavily));
			bool flag3 = ResolveDamageSignalChanged(targetPlayer.timeSinceTakingDamage);
			bool flag4 = ResolveCriticalSignalChanged(rawCriticalState);
			bool lowHealthSignalChanged = flag3 || flag4;
			int num = ResolveDisplayHealth(rawHealth, rawCriticalState, lowHealthSignalChanged, settings.CriticalHealthMode);
			int num2 = ResolveSteppedHealthDisplay(num);
			lastDebugRawHealth = rawHealth;
			lastDebugDisplayHealthTarget = num;
			lastDebugDisplayedHealth = num2;
			lastDebugRawCriticalState = rawCriticalState;
			lastDebugDamageSignalChanged = flag3;
			lastDebugCriticalSignalChanged = flag4;
			ApplyStripLayoutOffsets(settings);
			bool flag5 = true;
			LogDebugVisibility("visible", context.ViewCamera, distanceSqr);
			bool enabled = num < 20 || num2 < 20;
			healthStrip.SetFillColorOverride(enabled, CriticalHealthBarColor);
			if (flag5 && (!wasHealthVisible || num2 != lastDisplayedHealth || lastDisplayedHealthLabel != "HP"))
			{
				healthStrip.SetDisplay("HP", num2, 100f, showPercent: false);
				lastDisplayedHealth = num2;
				lastDisplayedHealthLabel = "HP";
			}
			healthStrip.SetVisible(flag5);
			wasHealthVisible = flag5;
			float infectionMeter = 0f;
			bool hasInfection = false;
			if (flag && snapshot.IsInfectionKnown)
			{
				hasInfection = snapshot.HasInfection;
				infectionMeter = snapshot.InfectionMeter;
			}
			else if (!PlayerStatusSnapshotSync.TryGetReportedInfection(targetPlayer, boundPlayerKey, out hasInfection, out infectionMeter) && flag2)
			{
				hasInfection = CadaverGrowthInfectionProvider.TryGetNormalizedInfection(targetPlayer, boundPlayerKey, out infectionMeter);
			}
			bool flag6 = ((settings.InfectionDisplayMode == StatusBarConfig.InfectionBarDisplayMode.AlwaysVisible) ? flag2 : (flag2 && hasInfection));
			if (flag6)
			{
				int num3 = ResolveDisplayInfectionPercent(infectionMeter, hasInfection);
				if (!wasInfectionVisible || num3 != lastDisplayedInfectionPercent)
				{
					infectionStrip.SetDisplay("INF", num3, 100f, showPercent: true);
					lastDisplayedInfectionPercent = num3;
				}
				infectionStrip.SetVisible(isVisible: true);
				wasInfectionVisible = true;
			}
			else
			{
				infectionStrip.SetVisible(isVisible: false);
				wasInfectionVisible = false;
				ResetInfectionPrediction();
				ResetDisplayedInfectionPercent();
				lastDisplayedInfectionPercent = int.MinValue;
			}
			SetCanvasEnabled(flag5 || flag6);
			ApplyStripsIfDirty(settings);
		}

		private void HideForDistance(Camera? viewCamera, float distanceSqr)
		{
			LogDebugVisibility("distance-hidden", viewCamera, distanceSqr);
			SetCanvasEnabled(enabled: false);
			healthStrip.SetVisible(isVisible: false);
			infectionStrip.SetVisible(isVisible: false);
			if (wasHealthVisible)
			{
				ResetDisplayedHealth();
			}
			if (wasInfectionVisible)
			{
				ResetInfectionPrediction();
				ResetDisplayedInfectionPercent();
			}
			wasHealthVisible = false;
			wasInfectionVisible = false;
		}

		private bool IsTargetReadyForDisplay(bool targetIsDead)
		{
			if ((Object)(object)targetPlayer != (Object)null && !targetPlayer.disconnectedMidGame && !targetIsDead)
			{
				return ((Component)targetPlayer).gameObject.activeInHierarchy;
			}
			return false;
		}

		private void ResetRuntimeState()
		{
			lastDisplayedHealth = int.MinValue;
			lastDisplayedInfectionPercent = int.MinValue;
			wasHealthVisible = false;
			wasInfectionVisible = false;
			lastDisplayedHealthLabel = string.Empty;
			lastDebugVisibilityState = string.Empty;
			nextDebugVisibilityLogTime = 0f;
			lastAppliedCanvasCamera = null;
			lastAppliedUiScale = -1f;
			lastAppliedHealthBarYOffset = float.NaN;
			lastAppliedInfectionBarYOffset = float.NaN;
			wasTargetDead = false;
			hasObservedRawHealth = false;
			ResetLowHealthSignals();
			ResetLowHealthPrediction();
			ResetNaturalRecoveredLowHealthHold();
			ResetDisplayedHealth();
			ResetDebugHealthDecisionState();
			ResetInfectionPrediction();
			ResetDisplayedInfectionPercent();
			wasUsingSnapshot = false;
		}

		private void HandleSnapshotSourceTransition(bool hasSyncedSnapshot)
		{
			if (wasUsingSnapshot != hasSyncedSnapshot)
			{
				wasUsingSnapshot = hasSyncedSnapshot;
				lastDisplayedHealth = int.MinValue;
				lastDisplayedHealthLabel = string.Empty;
				lastDisplayedInfectionPercent = int.MinValue;
				ResetLowHealthSignals();
				ResetLowHealthPrediction();
				ResetNaturalRecoveredLowHealthHold();
				ResetDisplayedHealth();
				ResetInfectionPrediction();
				ResetDisplayedInfectionPercent();
			}
		}

		private void HandleDeathStateTransition(bool targetIsDead)
		{
			if (targetIsDead)
			{
				if (!wasTargetDead)
				{
					wasTargetDead = true;
					lastDisplayedHealth = int.MinValue;
					lastDisplayedHealthLabel = string.Empty;
					ResetLowHealthSignals();
					ResetLowHealthPrediction();
					ResetNaturalRecoveredLowHealthHold();
					ResetDisplayedHealth();
					ResetInfectionPrediction();
					ResetDisplayedInfectionPercent();
				}
			}
			else if (wasTargetDead)
			{
				wasTargetDead = false;
				lastDisplayedHealth = int.MinValue;
				lastDisplayedHealthLabel = string.Empty;
				ResetLowHealthSignals();
				ResetLowHealthPrediction();
				ResetNaturalRecoveredLowHealthHold();
				ResetDisplayedHealth();
				ResetInfectionPrediction();
				ResetDisplayedInfectionPercent();
			}
		}

		private bool ResolveDamageSignalChanged(float damageTimestamp)
		{
			if (!hasObservedDamageTimestamp)
			{
				hasObservedDamageTimestamp = true;
				lastObservedDamageTimestamp = damageTimestamp;
				return false;
			}
			if (damageTimestamp == lastObservedDamageTimestamp)
			{
				return false;
			}
			lastObservedDamageTimestamp = damageTimestamp;
			return true;
		}

		private bool ResolveCriticalSignalChanged(bool rawCriticalState)
		{
			if (!hasObservedRawCriticalState)
			{
				hasObservedRawCriticalState = true;
				lastObservedRawCriticalState = rawCriticalState;
				return false;
			}
			if (rawCriticalState == lastObservedRawCriticalState)
			{
				return false;
			}
			lastObservedRawCriticalState = rawCriticalState;
			return true;
		}

		private int ResolveDisplayHealth(int rawHealth, bool rawCriticalState, bool lowHealthSignalChanged, StatusBarConfig.CriticalHealthSyncMode criticalHealthMode)
		{
			bool flag = !hasObservedRawHealth || rawHealth != lastObservedRawHealth;
			if (flag)
			{
				hasObservedRawHealth = true;
				lastObservedRawHealth = rawHealth;
			}
			if (rawHealth <= 0)
			{
				ResetLowHealthPrediction();
				ResetNaturalRecoveredLowHealthHold();
				return rawHealth;
			}
			if (holdNaturalRecoveredLowHealthDisplay)
			{
				if (!(rawHealth != naturalRecoveredRawHealth || lowHealthSignalChanged))
				{
					return 20;
				}
				ResetNaturalRecoveredLowHealthHold();
			}
			if (ShouldInferVanillaCriticalHealth(rawHealth, rawCriticalState, criticalHealthMode))
			{
				if (!isLowHealthPredictionActive || rawHealth != predictionObservedRawHealth)
				{
					BeginLowHealthPrediction(5, rawHealth);
				}
				return ResolveLowHealthPredictionDisplay(rawHealth, flag || lowHealthSignalChanged, 5);
			}
			if (rawHealth >= 20)
			{
				if (isLowHealthPredictionActive)
				{
					bool num = PredictLowHealthFromElapsed() >= 20 && !flag;
					ResetLowHealthPrediction();
					if (num)
					{
						BeginNaturalRecoveredLowHealthHold(rawHealth);
						return 20;
					}
				}
				return rawHealth;
			}
			ResetNaturalRecoveredLowHealthHold();
			return ResolveLowHealthPredictionDisplay(rawHealth, flag || lowHealthSignalChanged, rawHealth);
		}

		private static bool ShouldInferVanillaCriticalHealth(int rawHealth, bool rawCriticalState, StatusBarConfig.CriticalHealthSyncMode criticalHealthMode)
		{
			if (criticalHealthMode == StatusBarConfig.CriticalHealthSyncMode.VanillaPrediction && rawCriticalState)
			{
				return rawHealth == 20;
			}
			return false;
		}

		private int ResolveLowHealthPredictionDisplay(int rawHealth, bool restartLowHealthPrediction, int predictionStartHealth)
		{
			if (!isLowHealthPredictionActive || rawHealth != predictionObservedRawHealth || restartLowHealthPrediction)
			{
				BeginLowHealthPrediction(predictionStartHealth, rawHealth);
			}
			if (PredictLowHealthFromElapsed() >= 20 && !restartLowHealthPrediction)
			{
				ResetLowHealthPrediction();
				BeginNaturalRecoveredLowHealthHold(rawHealth);
				return 20;
			}
			return ResolvePredictedLowHealthDisplay();
		}

		private int ResolveSteppedHealthDisplay(int targetHealth)
		{
			if (!hasDisplayedHealth)
			{
				hasDisplayedHealth = true;
				displayedHealth = targetHealth;
				healthDisplayTarget = targetHealth;
				nextHealthDisplayStepTime = Time.unscaledTime + 0.05f;
				return displayedHealth;
			}
			if (targetHealth == displayedHealth)
			{
				healthDisplayTarget = targetHealth;
				return displayedHealth;
			}
			if (targetHealth != healthDisplayTarget)
			{
				healthDisplayTarget = targetHealth;
				nextHealthDisplayStepTime = 0f;
			}
			if (Time.unscaledTime < nextHealthDisplayStepTime)
			{
				return displayedHealth;
			}
			displayedHealth += ((targetHealth > displayedHealth) ? 1 : (-1));
			nextHealthDisplayStepTime = Time.unscaledTime + 0.05f;
			return displayedHealth;
		}

		private void BeginLowHealthPrediction(int displayHealth, int observedRawHealth)
		{
			isLowHealthPredictionActive = true;
			predictionObservedRawHealth = observedRawHealth;
			predictionStartHealth = displayHealth;
			predictedLowHealth = displayHealth;
			lowHealthPredictionStartTime = Time.time;
		}

		private int ResolvePredictedLowHealthDisplay()
		{
			return PredictLowHealthFromElapsed();
		}

		private int PredictLowHealthFromElapsed()
		{
			float num = Time.time - lowHealthPredictionStartTime;
			int num2 = Mathf.Max(0, Mathf.FloorToInt(num / 1f));
			predictedLowHealth = Mathf.Min(20, predictionStartHealth + num2);
			return predictedLowHealth;
		}

		private void BeginNaturalRecoveredLowHealthHold(int rawHealth)
		{
			holdNaturalRecoveredLowHealthDisplay = true;
			naturalRecoveredRawHealth = rawHealth;
		}

		private void ResetLowHealthPrediction()
		{
			isLowHealthPredictionActive = false;
			predictionObservedRawHealth = int.MinValue;
			predictionStartHealth = int.MinValue;
			predictedLowHealth = int.MinValue;
			lowHealthPredictionStartTime = 0f;
		}

		private void ResetLowHealthSignals()
		{
			hasObservedDamageTimestamp = false;
			lastObservedDamageTimestamp = 0f;
			hasObservedRawCriticalState = false;
			lastObservedRawCriticalState = false;
		}

		private void ResetDisplayedHealth()
		{
			hasDisplayedHealth = false;
			displayedHealth = int.MinValue;
			healthDisplayTarget = int.MinValue;
			nextHealthDisplayStepTime = 0f;
		}

		private void ResetDebugHealthDecisionState()
		{
			lastDebugRawHealth = int.MinValue;
			lastDebugDisplayHealthTarget = int.MinValue;
			lastDebugDisplayedHealth = int.MinValue;
			lastDebugRawCriticalState = false;
			lastDebugDamageSignalChanged = false;
			lastDebugCriticalSignalChanged = false;
		}

		private void ResetNaturalRecoveredLowHealthHold()
		{
			holdNaturalRecoveredLowHealthDisplay = false;
			naturalRecoveredRawHealth = int.MinValue;
		}

		private int ResolveDisplayInfectionPercent(float rawMeter, bool hasInfectionValue)
		{
			if (!hasInfectionValue)
			{
				ResetInfectionPrediction();
				ResetDisplayedInfectionPercent();
				return 0;
			}
			rawMeter = Mathf.Clamp01(rawMeter);
			float unscaledTime = Time.unscaledTime;
			if (!isInfectionPredictionActive)
			{
				BeginInfectionPrediction(rawMeter, 0f);
				lastObservedInfectionMeter = rawMeter;
				lastObservedInfectionSampleTime = unscaledTime;
				return ResolveSteppedInfectionPercent(MeterToInfectionPercent(rawMeter));
			}
			if (rawMeter < lastObservedInfectionMeter)
			{
				BeginInfectionPrediction(rawMeter, 0f);
			}
			else if (rawMeter > lastObservedInfectionMeter)
			{
				UpdateInfectionPredictionRate(rawMeter, unscaledTime);
			}
			lastObservedInfectionMeter = rawMeter;
			int targetPercent = MeterToInfectionPercent(PredictInfectionMeter());
			return ResolveSteppedInfectionPercent(targetPercent);
		}

		private int ResolveSteppedInfectionPercent(int targetPercent)
		{
			if (!hasDisplayedInfectionPercent)
			{
				hasDisplayedInfectionPercent = true;
				displayedInfectionPercent = targetPercent;
				nextInfectionDisplayStepTime = Time.unscaledTime + 0.1f;
				return displayedInfectionPercent;
			}
			if (targetPercent < displayedInfectionPercent)
			{
				displayedInfectionPercent = targetPercent;
				nextInfectionDisplayStepTime = Time.unscaledTime + 0.1f;
				return displayedInfectionPercent;
			}
			if (Time.unscaledTime < nextInfectionDisplayStepTime)
			{
				return displayedInfectionPercent;
			}
			displayedInfectionPercent = Mathf.Min(targetPercent, displayedInfectionPercent + 1);
			nextInfectionDisplayStepTime = Time.unscaledTime + 0.1f;
			return displayedInfectionPercent;
		}

		private void UpdateInfectionPredictionRate(float rawMeter, float now)
		{
			float num = Mathf.Max(0.5f, now - lastObservedInfectionSampleTime);
			float rate = (rawMeter - lastObservedInfectionMeter) / num;
			BeginInfectionPrediction(rawMeter, rate);
			lastObservedInfectionSampleTime = now;
		}

		private void BeginInfectionPrediction(float rawMeter, float rate)
		{
			isInfectionPredictionActive = true;
			infectionPredictionStartMeter = rawMeter;
			infectionPredictionStartTime = Time.unscaledTime;
			infectionPredictionRate = rate;
		}

		private float PredictInfectionMeter()
		{
			float num = Mathf.Min(Time.unscaledTime - infectionPredictionStartTime, 0.5f);
			return Mathf.Clamp01(infectionPredictionStartMeter + infectionPredictionRate * num);
		}

		private static int MeterToInfectionPercent(float meter)
		{
			return Mathf.Clamp(Mathf.FloorToInt(Mathf.Clamp01(meter) * 100f), 1, 99);
		}

		private void ResetInfectionPrediction()
		{
			isInfectionPredictionActive = false;
			lastObservedInfectionMeter = 0f;
			lastObservedInfectionSampleTime = 0f;
			infectionPredictionStartMeter = 0f;
			infectionPredictionStartTime = 0f;
			infectionPredictionRate = 0f;
		}

		private void ResetDisplayedInfectionPercent()
		{
			hasDisplayedInfectionPercent = false;
			displayedInfectionPercent = int.MinValue;
			lastDisplayedInfectionPercent = int.MinValue;
			nextInfectionDisplayStepTime = 0f;
		}

		private void ApplyCanvasScale(float uiScale)
		{
			//IL_0015: 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)
			if (!Mathf.Approximately(lastAppliedUiScale, uiScale))
			{
				((Transform)canvasRect).localScale = Vector3.one * uiScale;
				lastAppliedUiScale = uiScale;
			}
		}

		private void ApplyStripLayoutOffsets(StatusBarConfig settings)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			float healthBarYOffset = settings.HealthBarYOffset;
			if (!Mathf.Approximately(lastAppliedHealthBarYOffset, healthBarYOffset))
			{
				healthStripRect.anchoredPosition = new Vector2(0f, healthBarYOffset);
				lastAppliedHealthBarYOffset = healthBarYOffset;
			}
			float infectionBarYOffset = settings.InfectionBarYOffset;
			if (!Mathf.Approximately(lastAppliedInfectionBarYOffset, infectionBarYOffset))
			{
				infectionStripRect.anchoredPosition = new Vector2(0f, infectionBarYOffset);
				lastAppliedInfectionBarYOffset = infectionBarYOffset;
			}
		}

		private void SetBillboardRotation(Quaternion billboardRotation)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			((Component)this).transform.rotation = billboardRotation;
		}

		private void ApplyStripsIfDirty(StatusBarConfig settings)
		{
			healthStrip.ApplyIfDirty(settings);
			infectionStrip.ApplyIfDirty(settings);
		}

		private void SetCanvasEnabled(bool enabled)
		{
			if (lastCanvasEnabled != enabled || ((Behaviour)worldCanvas).enabled != enabled)
			{
				((Behaviour)worldCanvas).enabled = enabled;
				lastCanvasEnabled = enabled;
			}
		}

		private void ApplyCanvasCamera(Camera? viewCamera)
		{
			if (!((Object)(object)lastAppliedCanvasCamera == (Object)(object)viewCamera) || !((Object)(object)worldCanvas.worldCamera == (Object)(object)viewCamera))
			{
				worldCanvas.worldCamera = viewCamera;
				lastAppliedCanvasCamera = viewCamera;
			}
		}

		private void HideAll(StatusBarConfig settings, string reason, Camera? viewCamera, float distanceSqr)
		{
			LogDebugVisibility(reason, viewCamera, distanceSqr);
			SetCanvasEnabled(enabled: false);
			healthStrip.SetVisible(isVisible: false);
			infectionStrip.SetVisible(isVisible: false);
			wasHealthVisible = false;
			wasInfectionVisible = false;
			ResetInfectionPrediction();
			ResetDisplayedInfectionPercent();
			ApplyStripsIfDirty(settings);
		}

		private bool TryCreateStrip(string stripName, out PlayerStatusBarStrip strip)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			strip = null;
			GameObject val = new GameObject(stripName, new Type[1] { typeof(RectTransform) });
			val.transform.SetParent(((Component)worldCanvas).transform, false);
			((Object)val).name = stripName;
			val.transform.localPosition = Vector3.zero;
			val.transform.localRotation = Quaternion.identity;
			strip = val.AddComponent<PlayerStatusBarStrip>();
			strip.Initialize((!(stripName == "HealthStrip")) ? PlayerStatusBarStrip.StripType.Infection : PlayerStatusBarStrip.StripType.Health);
			return true;
		}

		private bool ShouldShowForDistance(in PlayerStatusFrameContext context, out float distanceSqr)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: 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_001b: Unknown result type (might be due to invalid IL or missing references)
			Vector3 val = context.ObserverPosition - ((Component)targetPlayer).transform.position;
			distanceSqr = ((Vector3)(ref val)).sqrMagnitude;
			return distanceSqr <= context.MaxDistanceSqr;
		}

		private static Transform? ResolveAnchor(PlayerControllerB player)
		{
			if ((Object)(object)player.playerGlobalHead != (Object)null)
			{
				return player.playerGlobalHead;
			}
			if (player.bodyParts != null && player.bodyParts.Length != 0 && (Object)(object)player.bodyParts[0] != (Object)null)
			{
				return player.bodyParts[0];
			}
			return ((Component)player).transform;
		}

		private void LogDebugVisibility(string state, Camera? viewCamera, float distanceSqr)
		{
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e1: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.Settings.DebugLogging && (!(state == lastDebugVisibilityState) || !(Time.unscaledTime < nextDebugVisibilityLogTime)))
			{
				lastDebugVisibilityState = state;
				nextDebugVisibilityLogTime = Time.unscaledTime + 5f;
				string text = FormatDebugDistance(distanceSqr);
				Plugin.LogDebug(string.Format("View playerId={0} clientId={1} actualClientId={2} boundActualClientId={3} snapshot={4} name='{5}' state={6} health={7} rawHealth={8} displayHealthTarget={9} displayHealth={10} rawCritical={11} damageSignalChanged={12} criticalSignalChanged={13} predictionActive={14} naturalHold={15} controlled={16} dead={17} distance={18} canvasEnabled={19} camera='{20}' anchor='{21}' position={22} target={23}.", PlayerId, targetPlayer.playerClientId, targetPlayer.actualClientId, boundActualClientId, wasUsingSnapshot, targetPlayer.playerUsername, state, targetPlayer.health, lastDebugRawHealth, lastDebugDisplayHealthTarget, lastDebugDisplayedHealth, lastDebugRawCriticalState, lastDebugDamageSignalChanged, lastDebugCriticalSignalChanged, isLowHealthPredictionActive, holdNaturalRecoveredLowHealthDisplay, targetPlayer.isPlayerControlled, targetPlayer.isPlayerDead, text, ((Behaviour)worldCanvas).enabled, ((Object)(object)viewCamera != (Object)null) ? ((Object)viewCamera).name : "none", ((Object)(object)anchor != (Object)null) ? ((Object)anchor).name : "none", FormatVector(((Component)this).transform.position), FormatVector(((Component)targetPlayer).transform.position)));
			}
		}

		private static string FormatDebugDistance(float distanceSqr)
		{
			if (!(distanceSqr >= 0f))
			{
				return "n/a";
			}
			return $"{Mathf.Sqrt(distanceSqr):0.0}/{Plugin.Settings.MaxDistance:0.0}";
		}

		private static string FormatVector(Vector3 value)
		{
			//IL_0005: 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_001b: Unknown result type (might be due to invalid IL or missing references)
			return $"({value.x:0.0},{value.y:0.0},{value.z:0.0})";
		}
	}
	internal readonly struct PlayerStatusFrameContext
	{
		public readonly StatusBarConfig Settings;

		public readonly PlayerControllerB LocalPlayer;

		public readonly Camera? ViewCamera;

		public readonly bool HasBillboardRotation;

		public readonly Quaternion BillboardRotation;

		public readonly bool CanShowGroup;

		public readonly Vector3 ObserverPosition;

		public readonly float MaxDistanceSqr;

		public readonly float AnchorYOffset;

		public readonly float UiScale;

		public PlayerStatusFrameContext(StatusBarConfig settings, PlayerControllerB localPlayer, Camera? viewCamera, bool hasBillboardRotation, Quaternion billboardRotation, bool canShowGroup, Vector3 observerPosition)
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			Settings = settings;
			LocalPlayer = localPlayer;
			ViewCamera = viewCamera;
			HasBillboardRotation = hasBillboardRotation;
			BillboardRotation = billboardRotation;
			CanShowGroup = canShowGroup;
			ObserverPosition = observerPosition;
			MaxDistanceSqr = settings.MaxDistanceSqr;
			AnchorYOffset = settings.AnchorYOffset;
			UiScale = settings.UiScale;
		}
	}
	internal readonly struct PlayerStatusSnapshot
	{
		public ulong ActualClientId { get; }

		public int Health { get; }

		public bool IsDead { get; }

		public bool IsCritical { get; }

		public bool IsInfectionKnown { get; }

		public bool HasInfection { get; }

		public float InfectionMeter { get; }

		public float ReceivedTime { get; }

		public PlayerStatusSnapshot(ulong actualClientId, int health, bool isDead, bool isCritical, bool isInfectionKnown, bool hasInfection, float infectionMeter, float receivedTime)
		{
			ActualClientId = actualClientId;
			Health = health;
			IsDead = isDead;
			IsCritical = isCritical;
			IsInfectionKnown = isInfectionKnown;
			HasInfection = hasInfection;
			InfectionMeter = infectionMeter;
			ReceivedTime = receivedTime;
		}
	}
	internal static class PlayerStatusSnapshotSync
	{
		[CompilerGenerated]
		private static class <>O
		{
			public static HandleNamedMessageDelegate <0>__HandleSnapshotMessage;

			public static HandleNamedMessageDelegate <1>__HandleInfectionReportMessage;
		}

		private const string SnapshotMessageName = "playerstatusbars.status.v1";

		private const string InfectionReportMessageName = "playerstatusbars.infection-report.v1";

		private const byte ProtocolVersion = 2;

		private const byte InfectionReportProtocolVersion = 1;

		private const byte DeadFlag = 1;

		private const byte CriticalFlag = 2;

		private const byte InfectionFlag = 4;

		private const byte InfectionKnownFlag = 8;

		private const float SnapshotInterval = 1f;

		private const float SnapshotTtl = 3f;

		private const float InfectionReportHeartbeatInterval = 0.5f;

		private const float InfectionReportTtl = 3f;

		private const int MinimumSnapshotCapacity = 16;

		private const int MinimumInfectionReportCapacity = 16;

		private const int SnapshotHeaderBytes = 2;

		private const int SnapshotEntryBytes = 12;

		private const int InfectionReportBytes = 12;

		private const int MaxSnapshotPayloadBytes = 2048;

		private const int MaxSnapshotEntries = 170;

		private static PlayerStatusSnapshot[] snapshots = Array.Empty<PlayerStatusSnapshot>();

		private static bool[] hasSnapshots = Array.Empty<bool>();

		private static bool[] hasInfectionReports = Array.Empty<bool>();

		private static ulong[] infectionReportActualClientIds = Array.Empty<ulong>();

		private static bool[] infectionReportHasInfection = Array.Empty<bool>();

		private static float[] infectionReportMeters = Array.Empty<float>();

		private static float[] infectionReportReceivedTimes = Array.Empty<float>();

		private static CustomMessagingManager? registeredMessagingManager;

		private static float nextSnapshotSendTime;

		private static float nextLocalInfectionReportSendTime;

		private static bool hasLocalInfectionReportState;

		private static byte lastLocalInfectionReportPlayerId;

		private static ulong lastLocalInfectionReportActualClientId;

		private static byte lastLocalInfectionReportFlags;

		private static byte lastLocalInfectionReportPercent;

		private static int validSnapshotLength;

		private static int validInfectionReportLength;

		public static void Tick(StartOfRound startOfRound, PlayerControllerB[] allPlayers)
		{
			NetworkManager singleton = NetworkManager.Singleton;
			CustomMessagingManager val = (((Object)(object)singleton != (Object)null) ? singleton.CustomMessagingManager : null);
			if ((Object)(object)singleton == (Object)null || val == null || !singleton.IsListening)
			{
				ClearSnapshots();
				ClearInfectionReports();
				ResetLocalInfectionReportState();
				registeredMessagingManager = null;
				return;
			}
			EnsureRegistered(val);
			SendLocalInfectionReport(singleton, val, startOfRound, allPlayers);
			if (singleton.IsServer && !(Time.unscaledTime < nextSnapshotSendTime))
			{
				nextSnapshotSendTime = Time.unscaledTime + 1f;
				SendSnapshotToClients(singleton, val, startOfRound, allPlayers);
			}
		}

		public static bool TryGetSnapshot(PlayerControllerB player, out PlayerStatusSnapshot snapshot)
		{
			snapshot = default(PlayerStatusSnapshot);
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton != (Object)null && singleton.IsServer)
			{
				return false;
			}
			int num = (int)player.playerClientId;
			if (num < 0 || num >= validSnapshotLength || !hasSnapshots[num])
			{
				return false;
			}
			PlayerStatusSnapshot playerStatusSnapshot = snapshots[num];
			if (playerStatusSnapshot.ActualClientId != player.actualClientId || Time.unscaledTime - playerStatusSnapshot.ReceivedTime > 3f)
			{
				hasSnapshots[num] = false;
				return false;
			}
			snapshot = playerStatusSnapshot;
			return true;
		}

		internal static void ClearSnapshots()
		{
			ClearSnapshotValues();
			ClearInfectionReports();
			ResetLocalInfectionReportState();
			nextSnapshotSendTime = 0f;
		}

		private static void ClearSnapshotValues()
		{
			Array.Clear(hasSnapshots, 0, validSnapshotLength);
			Array.Clear(snapshots, 0, validSnapshotLength);
			validSnapshotLength = 0;
		}

		private static void EnsureRegistered(CustomMessagingManager messagingManager)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Expected O, but got Unknown
			if (registeredMessagingManager != messagingManager)
			{
				if (registeredMessagingManager != null)
				{
					registeredMessagingManager.UnregisterNamedMessageHandler("playerstatusbars.status.v1");
					registeredMessagingManager.UnregisterNamedMessageHandler("playerstatusbars.infection-report.v1");
				}
				object obj = <>O.<0>__HandleSnapshotMessage;
				if (obj == null)
				{
					HandleNamedMessageDelegate val = HandleSnapshotMessage;
					<>O.<0>__HandleSnapshotMessage = val;
					obj = (object)val;
				}
				messagingManager.RegisterNamedMessageHandler("playerstatusbars.status.v1", (HandleNamedMessageDelegate)obj);
				object obj2 = <>O.<1>__HandleInfectionReportMessage;
				if (obj2 == null)
				{
					HandleNamedMessageDelegate val2 = HandleInfectionReportMessage;
					<>O.<1>__HandleInfectionReportMessage = val2;
					obj2 = (object)val2;
				}
				messagingManager.RegisterNamedMessageHandler("playerstatusbars.infection-report.v1", (HandleNamedMessageDelegate)obj2);
				registeredMessagingManager = messagingManager;
			}
		}

		private static void SendSnapshotToClients(NetworkManager networkManager, CustomMessagingManager messagingManager, StartOfRound startOfRound, PlayerControllerB[] allPlayers)
		{
			//IL_0038: 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)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_01dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_014f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0155: Unknown result type (might be due to invalid IL or missing references)
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_0168: Unknown result type (might be due to invalid IL or missing references)
			//IL_0175: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: 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)
			//IL_018e: Unknown result type (might be due to invalid IL or missing references)
			//IL_019b: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a1: Unknown result type (might be due to invalid IL or missing references)
			if (allPlayers == null || allPlayers.Length == 0)
			{
				ClearSnapshotValues();
				return;
			}
			if (!HasRemoteClients(networkManager))
			{
				ClearSnapshotValues();
				ClearInfectionReports();
				return;
			}
			FastBufferWriter val = default(FastBufferWriter);
			((FastBufferWriter)(ref val))..ctor(2048, (Allocator)2, -1);
			try
			{
				byte b = 2;
				byte b2 = 0;
				((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives));
				int position = ((FastBufferWriter)(ref val)).Position;
				((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b2, default(ForPrimitives));
				for (int i = 0; i < allPlayers.Length; i++)
				{
					if (b2 >= 170)
					{
						break;
					}
					PlayerControllerB val2 = allPlayers[i];
					if (!ShouldIncludePlayer(startOfRound, val2))
					{
						continue;
					}
					int num = (int)val2.playerClientId;
					if (num < 0 || num > 255)
					{
						continue;
					}
					int num2 = Mathf.Clamp(val2.health, 0, 255);
					byte b3 = 0;
					if (val2.isPlayerDead || num2 <= 0)
					{
						b3 = (byte)(b3 | 1u);
					}
					if (val2.criticallyInjured || val2.bleedingHeavily)
					{
						b3 = (byte)(b3 | 2u);
					}
					bool hasInfection;
					float infectionMeter;
					bool flag = TryGetReportedInfection(val2, i, out hasInfection, out infectionMeter);
					if (!flag)
					{
						flag = CadaverGrowthInfectionProvider.TryGetInfectionState(val2, i, out hasInfection, out infectionMeter);
					}
					byte b4 = 0;
					if (flag)
					{
						b3 = (byte)(b3 | 8u);
						b4 = (byte)Mathf.Clamp(Mathf.RoundToInt(infectionMeter * 100f), 0, 100);
						if (hasInfection && b4 > 0)
						{
							b3 = (byte)(b3 | 4u);
						}
					}
					byte b5 = (byte)num;
					ulong actualClientId = val2.actualClientId;
					byte b6 = (byte)num2;
					((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b5, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref actualClientId, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b6, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b3, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b4, default(ForPrimitives));
					b2++;
				}
				int position2 = ((FastBufferWriter)(ref val)).Position;
				((FastBufferWriter)(ref val)).Seek(position);
				((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b2, default(ForPrimitives));
				((FastBufferWriter)(ref val)).Seek(position2);
				messagingManager.SendNamedMessageToAll("playerstatusbars.status.v1", val, (NetworkDelivery)3);
			}
			finally
			{
				((IDisposable)(FastBufferWriter)(ref val)).Dispose();
			}
		}

		private static void SendLocalInfectionReport(NetworkManager networkManager, CustomMessagingManager messagingManager, StartOfRound startOfRound, PlayerControllerB[] allPlayers)
		{
			//IL_00fb: 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_010e: 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)
			//IL_0121: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: Unknown result type (might be due to invalid IL or missing references)
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0147: Unknown result type (might be due to invalid IL or missing references)
			//IL_014d: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			if (!networkManager.IsClient || networkManager.IsServer)
			{
				return;
			}
			PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : startOfRound?.localPlayerController);
			if ((Object)(object)val == (Object)null || allPlayers == null || allPlayers.Length == 0)
			{
				return;
			}
			int num = (int)val.playerClientId;
			if (num < 0 || num > 255)
			{
				return;
			}
			int knownPlayerSlot = ResolvePlayerSlot(val, allPlayers, num);
			bool flag;
			bool hasInfection;
			float infectionMeter;
			if (val.isPlayerDead || val.health <= 0)
			{
				flag = true;
				hasInfection = false;
				infectionMeter = 0f;
			}
			else
			{
				flag = CadaverGrowthInfectionProvider.TryGetInfectionState(val, knownPlayerSlot, out hasInfection, out infectionMeter);
			}
			byte b = 0;
			if (flag)
			{
				b = (byte)(b | 8u);
			}
			byte b2 = 0;
			if (flag && hasInfection)
			{
				b2 = (byte)Mathf.Clamp(Mathf.RoundToInt(infectionMeter * 100f), 0, 100);
				if (b2 > 0)
				{
					b = (byte)(b | 4u);
				}
			}
			byte playerId = (byte)num;
			ulong actualClientId = val.actualClientId;
			if (!ShouldSendLocalInfectionReport(playerId, actualClientId, b, b2))
			{
				return;
			}
			FastBufferWriter val2 = default(FastBufferWriter);
			((FastBufferWriter)(ref val2))..ctor(12, (Allocator)2, -1);
			try
			{
				byte b3 = 1;
				((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b3, default(ForPrimitives));
				((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref playerId, default(ForPrimitives));
				((FastBufferWriter)(ref val2)).WriteValueSafe<ulong>(ref actualClientId, default(ForPrimitives));
				((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives));
				((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b2, default(ForPrimitives));
				messagingManager.SendNamedMessage("playerstatusbars.infection-report.v1", 0uL, val2, (NetworkDelivery)3);
				hasLocalInfectionReportState = true;
				lastLocalInfectionReportPlayerId = playerId;
				lastLocalInfectionReportActualClientId = actualClientId;
				lastLocalInfectionReportFlags = b;
				lastLocalInfectionReportPercent = b2;
				nextLocalInfectionReportSendTime = Time.unscaledTime + 0.5f;
			}
			finally
			{
				((IDisposable)(FastBufferWriter)(ref val2)).Dispose();
			}
		}

		private static bool ShouldSendLocalInfectionReport(byte playerId, ulong actualClientId, byte flags, byte infectionPercent)
		{
			if (!hasLocalInfectionReportState)
			{
				return (flags & 8) != 0;
			}
			if (playerId != lastLocalInfectionReportPlayerId || actualClientId != lastLocalInfectionReportActualClientId || flags != lastLocalInfectionReportFlags || infectionPercent != lastLocalInfectionReportPercent)
			{
				return true;
			}
			return Time.unscaledTime >= nextLocalInfectionReportSendTime;
		}

		private static bool HasRemoteClients(NetworkManager networkManager)
		{
			foreach (ulong connectedClientsId in networkManager.ConnectedClientsIds)
			{
				if (connectedClientsId != 0L)
				{
					return true;
				}
			}
			return false;
		}

		private static void HandleSnapshotMessage(ulong senderClientId, FastBufferReader reader)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: 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)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: 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_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)NetworkManager.Singleton == (Object)null || senderClientId != 0L || !((FastBufferReader)(ref reader)).TryBeginRead(2))
			{
				return;
			}
			byte b = 0;
			byte b2 = 0;
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives));
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b2, default(ForPrimitives));
			if (b != 2)
			{
				return;
			}
			ClearSnapshotValues();
			float unscaledTime = Time.unscaledTime;
			for (int i = 0; i < b2; i++)
			{
				if (!((FastBufferReader)(ref reader)).TryBeginRead(12))
				{
					break;
				}
				byte b3 = 0;
				ulong actualClientId = 0uL;
				byte health = 0;
				byte b4 = 0;
				byte b5 = 0;
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b3, default(ForPrimitives));
				((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref actualClientId, default(ForPrimitives));
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref health, default(ForPrimitives));
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b4, default(ForPrimitives));
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b5, default(ForPrimitives));
				EnsureSnapshotCapacity(b3 + 1);
				snapshots[b3] = new PlayerStatusSnapshot(actualClientId, health, (b4 & 1) != 0, (b4 & 2) != 0, (b4 & 8) != 0, (b4 & 4) != 0, Mathf.Clamp01((float)(int)b5 / 100f), unscaledTime);
				hasSnapshots[b3] = true;
			}
		}

		private static void HandleInfectionReportMessage(ulong senderClientId, FastBufferReader reader)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsServer || !((FastBufferReader)(ref reader)).TryBeginRead(12))
			{
				return;
			}
			byte b = 0;
			byte reportedPlayerId = 0;
			ulong num = 0uL;
			byte b2 = 0;
			byte b3 = 0;
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives));
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref reportedPlayerId, default(ForPrimitives));
			((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref num, default(ForPrimitives));
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b2, default(ForPrimitives));
			((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b3, default(ForPrimitives));
			if (b == 1 && num == senderClientId && TryResolveReportedPlayerId(senderClientId, reportedPlayerId, out var playerId))
			{
				EnsureInfectionReportCapacity(playerId + 1);
				if ((b2 & 8) == 0)
				{
					hasInfectionReports[playerId] = false;
					return;
				}
				infectionReportActualClientIds[playerId] = num;
				infectionReportHasInfection[playerId] = (b2 & 4u) != 0 && b3 > 0;
				infectionReportMeters[playerId] = Mathf.Clamp01((float)(int)b3 / 100f);
				infectionReportReceivedTimes[playerId] = Time.unscaledTime;
				hasInfectionReports[playerId] = true;
			}
		}

		private static bool ShouldIncludePlayer(StartOfRound startOfRound, PlayerControllerB player)
		{
			if ((Object)(object)startOfRound == (Object)null || (Object)(object)player == (Object)null || player.disconnectedMidGame || !((Component)player).gameObject.activeInHierarchy)
			{
				return false;
			}
			if (player.isPlayerControlled)
			{
				return true;
			}
			return startOfRound.ClientPlayerList.ContainsKey(player.actualClientId);
		}

		private static int ResolvePlayerSlot(PlayerControllerB player, PlayerControllerB[] allPlayers, int fallbackPlayerId)
		{
			if ((uint)fallbackPlayerId < (uint)allPlayers.Length && (Object)(object)allPlayers[fallbackPlayerId] == (Object)(object)player)
			{
				return fallbackPlayerId;
			}
			for (int i = 0; i < allPlayers.Length; i++)
			{
				if ((Object)(object)allPlayers[i] == (Object)(object)player)
				{
					return i;
				}
			}
			return fallbackPlayerId;
		}

		private static bool TryResolveReportedPlayerId(ulong senderClientId, byte reportedPlayerId, out int playerId)
		{
			playerId = -1;
			StartOfRound instance = StartOfRound.Instance;
			PlayerControllerB[] array = instance?.allPlayerScripts;
			if ((Object)(object)instance == (Object)null || array == null)
			{
				return false;
			}
			if (instance.ClientPlayerList.TryGetValue(senderClientId, out var value) && (uint)value < (uint)array.Length && (Object)(object)array[value] != (Object)null && array[value].actualClientId == senderClientId)
			{
				playerId = value;
				return true;
			}
			if (reportedPlayerId < array.Length && (Object)(object)array[reportedPlayerId] != (Object)null && array[reportedPlayerId].actualClientId == senderClientId)
			{
				playerId = reportedPlayerId;
				return true;
			}
			return false;
		}

		internal static bool TryGetReportedInfection(PlayerControllerB player, int knownPlayerSlot, out bool hasInfection, out float infectionMeter)
		{
			hasInfection = false;
			infectionMeter = 0f;
			if ((Object)(object)player == (Object)null)
			{
				return false;
			}
			if (TryGetReportedInfectionAtIndex(knownPlayerSlot, player, out hasInfection, out infectionMeter))
			{
				return true;
			}
			int num = (int)player.playerClientId;
			if (num != knownPlayerSlot)
			{
				return TryGetReportedInfectionAtIndex(num, player, out hasInfection, out infectionMeter);
			}
			return false;
		}

		private static bool TryGetReportedInfectionAtIndex(int index, PlayerControllerB player, out bool hasInfection, out float infectionMeter)
		{
			hasInfection = false;
			infectionMeter = 0f;
			if (index < 0 || index >= validInfectionReportLength || !hasInfectionReports[index])
			{
				return false;
			}
			if (infectionReportActualClientIds[index] != player.actualClientId || Time.unscaledTime - infectionReportReceivedTimes[index] > 3f)
			{
				hasInfectionReports[index] = false;
				return false;
			}
			hasInfection = infectionReportHasInfection[index];
			infectionMeter = infectionReportMeters[index];
			return true;
		}

		private static void EnsureSnapshotCapacity(int requiredLength)
		{
			if (requiredLength > validSnapshotLength)
			{
				if (hasSnapshots.Length < requiredLength)
				{
					int newSize = GrowCapacity(requiredLength, 16);
					Array.Resize(ref hasSnapshots, newSize);
					Array.Resize(ref snapshots, newSize);
				}
				validSnapshotLength = requiredLength;
			}
		}

		private static void EnsureInfectionReportCapacity(int requiredLength)
		{
			if (requiredLength > validInfectionReportLength)
			{
				if (hasInfectionReports.Length < requiredLength)
				{
					int newSize = GrowCapacity(requiredLength, 16);
					Array.Resize(ref hasInfectionReports, newSize);
					Array.Resize(ref infectionReportActualClientIds, newSize);
					Array.Resize(ref infectionReportHasInfection, newSize);
					Array.Resize(ref infectionReportMeters, newSize);
					Array.Resize(ref infectionReportReceivedTimes, newSize);
				}
				validInfectionReportLength = requiredLength;
			}
		}

		private static void ClearInfectionReports()
		{
			Array.Clear(hasInfectionReports, 0, validInfectionReportLength);
			Array.Clear(infectionReportActualClientIds, 0, validInfectionReportLength);
			Array.Clear(infectionReportHasInfection, 0, validInfectionReportLength);
			Array.Clear(infectionReportMeters, 0, validInfectionReportLength);
			Array.Clear(infectionReportReceivedTimes, 0, validInfectionReportLength);
			validInfectionReportLength = 0;
		}

		private static void ResetLocalInfectionReportState()
		{
			nextLocalInfectionReportSendTime = 0f;
			hasLocalInfectionReportState = false;
			lastLocalInfectionReportPlayerId = 0;
			lastLocalInfectionReportActualClientId = 0uL;
			lastLocalInfectionReportFlags = 0;
			lastLocalInfectionReportPercent = 0;
		}

		private static int GrowCapacity(int requiredCapacity, int minimumCapacity)
		{
			int num;
			for (num = minimumCapacity; num < requiredCapacity; num *= 2)
			{
			}
			return num;
		}
	}
	[BepInPlugin("playerstatusbars", "PlayerStatusBars", "0.2.1")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private const string RuntimeObjectName = "PlayerStatusBars.Runtime";

		private const string LegacyConfigFileName = "Codex.OtherPlayerStatusBars.cfg";

		private const string LcChineseProjectGuid = "cn.codex.v81testchn";

		private const string LcChineseProjectConfigFileName = "LC Chinese Project.cfg";

		private const string LcChineseProjectPluginDirectoryName = "V81TestChn";

		private const string LcChineseProjectPluginFileName = "V81TestChn.dll";

		private static GameObject? runtimeObject;

		private static PlayerStatusBarManager? runtimeManager;

		internal static ManualLogSource Log { get; private set; }

		internal static StatusBarConfig Settings { get; private set; }

		internal static bool UseChineseCriticalLabel { get; private set; }

		internal static bool UseChineseConfigDescriptions { get; private set; }

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			MigrateLegacyConfigFile();
			UseChineseConfigDescriptions = DetectLcChineseProject();
			UseChineseCriticalLabel = UseChineseConfigDescriptions;
			Settings = StatusBarConfig.Create(((BaseUnityPlugin)this).Config, UseChineseConfigDescriptions);
			EnsureRuntimeManager();
			((BaseUnityPlugin)this).Config.Save();
			Log.LogInfo((object)"PlayerStatusBars loaded.");
			LogDebug("Debug logging is enabled.");
		}

		private void OnDestroy()
		{
			LogDebug("Plugin component destroyed; runtime manager remains active.");
		}

		internal static void LogDebug(string message)
		{
			StatusBarConfig settings = Settings;
			if (settings != null && settings.DebugLogging)
			{
				Log.LogInfo((object)("[Debug] " + message));
			}
		}

		private void MigrateLegacyConfigFile()
		{
			string configFilePath = ((BaseUnityPlugin)this).Config.ConfigFilePath;
			string directoryName = Path.GetDirectoryName(configFilePath);
			if (!string.IsNullOrEmpty(directoryName))
			{
				string text = Path.Combine(directoryName, "Codex.OtherPlayerStatusBars.cfg");
				if (File.Exists(text) && !File.Exists(configFilePath))
				{
					File.Copy(text, configFilePath);
					((BaseUnityPlugin)this).Config.Reload();
					Log.LogInfo((object)("Migrated legacy config from Codex.OtherPlayerStatusBars.cfg to " + Path.GetFileName(configFilePath) + "."));
				}
			}
		}

		private static void EnsureRuntimeManager()
		{
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Expected O, but got Unknown
			if ((Object)(object)runtimeManager != (Object)null)
			{
				GameObject gameObject = ((Component)runtimeManager).gameObject;
				if ((Object)(object)gameObject != (Object)null && !gameObject.activeSelf)
				{
					gameObject.SetActive(true);
				}
				LogDebug("Runtime manager already active.");
				return;
			}
			if ((Object)(object)runtimeObject == (Object)null)
			{
				runtimeObject = new GameObject("PlayerStatusBars.Runtime");
				((Object)runtimeObject).hideFlags = (HideFlags)61;
				Object.DontDestroyOnLoad((Object)(object)runtimeObject);
			}
			else if (!runtimeObject.activeSelf)
			{
				runtimeObject.SetActive(true);
			}
			runtimeManager = runtimeObject.GetComponent<PlayerStatusBarManager>();
			if ((Object)(object)runtimeManager == (Object)null)
			{
				runtimeManager = runtimeObject.AddComponent<PlayerStatusBarManager>();
			}
			LogDebug("Runtime manager created.");
		}

		private static bool DetectLcChineseProject()
		{
			foreach (PluginInfo value3 in Chainloader.PluginInfos.Values)
			{
				string guid = value3.Metadata.GUID ?? string.Empty;
				string value = value3.Metadata.Name ?? string.Empty;
				string value2 = value3.Location ?? string.Empty;
				if (IsLcChineseProjectGuid(guid) || LooksLikeLcChineseProject(value) || LooksLikeLcChineseProject(value2))
				{
					return true;
				}
			}
			if (File.Exists(Path.Combine(Paths.ConfigPath, "LC Chinese Project.cfg")))
			{
				return true;
			}
			if (File.Exists(Path.Combine(Paths.PluginPath, "V81TestChn", "V81TestChn.dll")))
			{
				return true;
			}
			return false;
		}

		private static bool IsLcChineseProjectGuid(string guid)
		{
			return string.Equals(guid, "cn.codex.v81testchn", StringComparison.OrdinalIgnoreCase);
		}

		private static bool LooksLikeLcChineseProject(string value)
		{
			if (!Contains(value, "cn.codex.v81testchn") && !Contains(value, "LC Chinese Project") && !Contains(value, "V81TestChn"))
			{
				return Contains(value, "V81TestChn.dll");
			}
			return true;
		}

		private static bool Contains(string value, string term)
		{
			return value.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
		}
	}
	internal static class StatusBarBillboard
	{
		private static int cachedCameraFrame = -1;

		private static Camera? cachedCamera;