Decompiled source of BoatRace v0.3.0

BoatRace.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BoatRace.Commands;
using BoatRace.Config;
using BoatRace.Diagnostics;
using BoatRace.Markers;
using BoatRace.Networking;
using BoatRace.Race;
using BoatRace.UI;
using HarmonyLib;
using Jotunn;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using SimpleJson;
using Splatform;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("BoatRace")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+bda3f577e129a51295fd8d74bf859c50ccb74320")]
[assembly: AssemblyProduct("BoatRace")]
[assembly: AssemblyTitle("BoatRace")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace BoatRace
{
	[BepInPlugin("com.boatrace.valheim", "BoatRace", "0.3.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGuid = "com.boatrace.valheim";

		public const string ModName = "BoatRace";

		public const string ModVersion = "0.3.0";

		public static readonly Color ActiveMarkerColor = new Color(0.3f, 1f, 0.4f, 1f);

		public static readonly Color InactiveMarkerColor = new Color(0.75f, 0.75f, 0.78f, 1f);

		private static GameObject updaterHost;

		private Harmony harmony;

		public static Plugin Instance { get; private set; }

		public static ConfigEntry<float> ConfigPositionX { get; private set; }

		public static ConfigEntry<float> ConfigPositionY { get; private set; }

		public static ConfigEntry<float> ConfigPenaltySeconds { get; private set; }

		public static ConfigEntry<float> ConfigCountdownSeconds { get; private set; }

		public static ConfigEntry<float> ConfigDetectionInterval { get; private set; }

		public static ConfigEntry<float> ConfigGateSpanMargin { get; private set; }

		public static ConfigEntry<float> ConfigMaxSampleJump { get; private set; }

		public static ConfigEntry<float> ConfigSyncInterval { get; private set; }

		public static ConfigEntry<float> ConfigMarkerScale { get; private set; }

		public static ConfigEntry<float> ConfigMarkerEmission { get; private set; }

		public static ConfigEntry<float> ConfigArrowEdgeMargin { get; private set; }

		public RaceManager RaceManager { get; private set; }

		public RpcManager RpcManager { get; private set; }

		public RaceHud Hud { get; private set; }

		private void Awake()
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Expected O, but got Unknown
			Instance = this;
			updaterHost = new GameObject("BoatRace_UpdaterHost");
			Object.DontDestroyOnLoad((Object)(object)updaterHost);
			BindConfig();
			RaceManager = new RaceManager();
			RpcManager = new RpcManager();
			updaterHost.AddComponent<ServerTicker>();
			new BoatRaceCommands();
			GateConfig.Load();
			PrefabManager.OnVanillaPrefabsAvailable += InitUI;
			ApplyPatches();
			Logger.LogInfo((object)"BoatRace v0.3.0 loaded.");
		}

		private void ApplyPatches()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			harmony = new Harmony("com.boatrace.valheim");
			try
			{
				harmony.PatchAll();
			}
			catch (Exception ex)
			{
				Logger.LogError((object)("[BoatRace] HARMONY PATCHING FAILED: " + ex.Message));
				Logger.LogError((object)"[BoatRace] A reconnecting client will NOT be sent race state, and will keep whatever it had before disconnecting.");
				return;
			}
			MethodInfo methodInfo = AccessTools.Method(typeof(ZNet), "RPC_PeerInfo", (Type[])null, (Type[])null);
			if (methodInfo == null)
			{
				Logger.LogError((object)"[BoatRace] Could not resolve ZNet.RPC_PeerInfo -- the peer-connect state push is NOT active. A reconnecting client will keep stale race state.");
				return;
			}
			bool flag = false;
			foreach (MethodBase patchedMethod in harmony.GetPatchedMethods())
			{
				if (patchedMethod == methodInfo)
				{
					flag = true;
					break;
				}
			}
			if (flag)
			{
				Logger.LogInfo((object)"[BoatRace] Harmony patch target resolved: ZNet.RPC_PeerInfo (per-connection race-state push active).");
			}
			else
			{
				Logger.LogError((object)"[BoatRace] ZNet.RPC_PeerInfo resolved but is NOT patched -- the per-connection race-state push is inactive.");
			}
		}

		private void BindConfig()
		{
			ConfigPositionX = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "PanelPositionX", 20f, "Horizontal position of the race leaderboard panel from the left edge.");
			ConfigPositionY = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "PanelPositionY", -180f, "Vertical position of the leaderboard panel from the top edge (negative = downward).");
			ConfigArrowEdgeMargin = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "ArrowEdgeMargin", 70f, "How far in from the screen edge (pixels) the direction arrow is clamped.");
			ConfigPenaltySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Race", "OutOfSequencePenaltySeconds", 15f, "Flat time penalty (seconds) added to a racer's elapsed time each time they cross a gate out of sequence.");
			ConfigCountdownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Race", "CountdownSeconds", 10f, "Length of the synced countdown before the race timer starts. Counted down on every client's screen as 10, 9, 8 ... 1, GO! Long enough that racers can get lined up on the start line; only the SERVER's value governs the race.");
			ConfigDetectionInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Race", "DetectionIntervalSeconds", 0.2f, "How often (seconds) the server samples player positions for gate crossings. Detection tests the segment between samples, so this does not need to be small.");
			ConfigGateSpanMargin = ((BaseUnityPlugin)this).Config.Bind<float>("Race", "GateSpanMarginMeters", 10f, "How far past either buoy a crossing still counts, in metres. Prevents a racer rounding the outside of a buoy from registering.");
			ConfigMaxSampleJump = ((BaseUnityPlugin)this).Config.Bind<float>("Race", "MaxSampleJumpMeters", 100f, "A position change larger than this between two samples is treated as a teleport and ignored rather than tested for gate crossings.");
			ConfigSyncInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "SyncIntervalSeconds", 1f, "How often (seconds) the server broadcasts race state to all clients.");
			ConfigMarkerScale = ((BaseUnityPlugin)this).Config.Bind<float>("Markers", "MarkerScale", 3f, "Scale multiplier applied to gate buoy markers. Larger is easier to see at distance over open water.");
			ConfigMarkerEmission = ((BaseUnityPlugin)this).Config.Bind<float>("Markers", "MarkerEmission", 0.8f, "Emission strength of the marker tint. Higher glows more at night and at distance.");
		}

		private void InitUI()
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Invalid comparison between Unknown and I4
			PrefabManager.OnVanillaPrefabsAvailable -= InitUI;
			if ((int)SystemInfo.graphicsDeviceType == 4)
			{
				Logger.LogInfo((object)"[BoatRace] Headless server detected -- skipping all HUD creation.");
				return;
			}
			Hud = updaterHost.AddComponent<RaceHud>();
			Logger.LogInfo((object)"[BoatRace] HUD initialised.");
		}
	}
}
namespace BoatRace.UI
{
	public class CountdownDisplay
	{
		private int lastShownSecond;

		private bool countdownWasRunning;

		private RacePhase lastPhase;

		public bool IsCountingDown => countdownWasRunning;

		public int LastShownSecond => lastShownSecond;

		public void UpdateFrame(RaceManager race)
		{
			if (race == null)
			{
				return;
			}
			RacePhase phase = race.Phase;
			RacePhase racePhase = lastPhase;
			lastPhase = phase;
			switch (phase)
			{
			case RacePhase.Countdown:
				TickCountdown(race.CountdownRemaining);
				return;
			case RacePhase.Racing:
				if (racePhase == RacePhase.Countdown && countdownWasRunning)
				{
					ShowGo();
				}
				Reset();
				return;
			}
			if (countdownWasRunning)
			{
				ClearCentreMessage();
				Logger.LogInfo((object)$"[BoatRace] Countdown cleared -- race went to {phase} before starting.");
			}
			Reset();
		}

		private void TickCountdown(float remaining)
		{
			countdownWasRunning = true;
			int num = Mathf.CeilToInt(remaining);
			if (num < 1)
			{
				num = 1;
			}
			if (num != lastShownSecond)
			{
				lastShownSecond = num;
				ShowCentre(num.ToString());
			}
		}

		private void ShowGo()
		{
			ShowCentre("GO!");
		}

		private void ClearCentreMessage()
		{
			ShowCentre(string.Empty);
		}

		private static void ShowCentre(string text)
		{
			if (!((Object)(object)MessageHud.instance == (Object)null))
			{
				MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
			}
		}

		private void Reset()
		{
			lastShownSecond = 0;
			countdownWasRunning = false;
		}

		public void NotifyWorldExit()
		{
			Reset();
			lastPhase = RacePhase.Idle;
		}
	}
	public class DirectionArrowUI
	{
		private GameObject canvasRoot;

		private GameObject arrow;

		private RectTransform arrowRect;

		private Text distanceText;

		private RectTransform distanceRect;

		private bool visible;

		private bool hasTarget;

		private bool ArrowExists
		{
			get
			{
				if ((Object)(object)arrow != (Object)null)
				{
					return Object.op_Implicit((Object)(object)arrow);
				}
				return false;
			}
		}

		public bool HasLiveTarget
		{
			get
			{
				if (visible && hasTarget)
				{
					return ArrowExists;
				}
				return false;
			}
		}

		public bool IsVisible => visible;

		public void SetVisible(bool value)
		{
			visible = value;
			if (ArrowExists)
			{
				arrow.SetActive(value);
			}
			if ((Object)(object)distanceText != (Object)null)
			{
				((Component)distanceText).gameObject.SetActive(value);
			}
		}

		private void Create()
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Expected O, but got Unknown
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_016b: Expected O, but got Unknown
			//IL_019e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_0254: Unknown result type (might be due to invalid IL or missing references)
			canvasRoot = new GameObject("BoatRaceArrowCanvas", new Type[2]
			{
				typeof(RectTransform),
				typeof(Canvas)
			});
			Object.DontDestroyOnLoad((Object)(object)canvasRoot);
			Canvas component = canvasRoot.GetComponent<Canvas>();
			component.renderMode = (RenderMode)0;
			component.sortingOrder = 99;
			arrow = new GameObject("BoatRaceArrow", new Type[2]
			{
				typeof(RectTransform),
				typeof(Image)
			});
			arrow.transform.SetParent(canvasRoot.transform, false);
			arrowRect = arrow.GetComponent<RectTransform>();
			arrowRect.sizeDelta = new Vector2(48f, 48f);
			arrowRect.anchorMin = new Vector2(0f, 0f);
			arrowRect.anchorMax = new Vector2(0f, 0f);
			arrowRect.pivot = new Vector2(0.5f, 0.5f);
			Image component2 = arrow.GetComponent<Image>();
			component2.sprite = BuildArrowSprite();
			((Graphic)component2).color = Plugin.ActiveMarkerColor;
			((Graphic)component2).raycastTarget = false;
			GameObject val = new GameObject("BoatRaceArrowDistance", new Type[2]
			{
				typeof(RectTransform),
				typeof(Text)
			});
			val.transform.SetParent(canvasRoot.transform, false);
			distanceRect = val.GetComponent<RectTransform>();
			distanceRect.sizeDelta = new Vector2(160f, 24f);
			distanceRect.anchorMin = new Vector2(0f, 0f);
			distanceRect.anchorMax = new Vector2(0f, 0f);
			distanceRect.pivot = new Vector2(0.5f, 0.5f);
			distanceText = val.GetComponent<Text>();
			distanceText.font = (((Object)(object)GUIManager.Instance.AveriaSerifBold != (Object)null) ? GUIManager.Instance.AveriaSerifBold : GUIManager.Instance.AveriaSerif);
			distanceText.fontSize = 14;
			distanceText.alignment = (TextAnchor)4;
			((Graphic)distanceText).color = Color.white;
			((Graphic)distanceText).raycastTarget = false;
			arrow.SetActive(visible);
			val.SetActive(visible);
		}

		private static Sprite BuildArrowSprite()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Expected O, but got Unknown
			//IL_00df: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			Texture2D val = new Texture2D(64, 64, (TextureFormat)4, false);
			Color32[] array = (Color32[])(object)new Color32[4096];
			for (int i = 0; i < 64; i++)
			{
				float num = (float)i / 63f;
				float num2 = (1f - num) * 32f;
				for (int j = 0; j < 64; j++)
				{
					bool flag = Mathf.Abs((float)j - 31.5f) <= num2 && num >= 0.25f;
					array[i * 64 + j] = (flag ? new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue) : new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)0));
				}
			}
			val.SetPixels32(array);
			val.Apply();
			((Texture)val).wrapMode = (TextureWrapMode)1;
			return Sprite.Create(val, new Rect(0f, 0f, 64f, 64f), new Vector2(0.5f, 0.5f));
		}

		public void UpdateFrame(bool hasTarget, Vector3 targetWorldPos)
		{
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0146: Unknown result type (might be due to invalid IL or missing references)
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0187: 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)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: 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_0110: Unknown result type (might be due to invalid IL or missing references)
			//IL_011a: Unknown result type (might be due to invalid IL or missing references)
			//IL_011f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_0194: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_021d: Unknown result type (might be due to invalid IL or missing references)
			//IL_021e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0220: Unknown result type (might be due to invalid IL or missing references)
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_0224: Unknown result type (might be due to invalid IL or missing references)
			//IL_0229: 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_01fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_020a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0231: Unknown result type (might be due to invalid IL or missing references)
			//IL_0238: Unknown result type (might be due to invalid IL or missing references)
			//IL_023f: Unknown result type (might be due to invalid IL or missing references)
			//IL_026b: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_02db: Unknown result type (might be due to invalid IL or missing references)
			//IL_0293: Unknown result type (might be due to invalid IL or missing references)
			//IL_0298: Unknown result type (might be due to invalid IL or missing references)
			this.hasTarget = hasTarget;
			if (!visible || !hasTarget)
			{
				if (ArrowExists)
				{
					arrow.SetActive(false);
				}
				if ((Object)(object)distanceText != (Object)null)
				{
					((Component)distanceText).gameObject.SetActive(false);
				}
				return;
			}
			if (!ArrowExists)
			{
				if (GUIManager.Instance == null)
				{
					return;
				}
				Create();
				if (!ArrowExists)
				{
					return;
				}
			}
			Camera main = Camera.main;
			if ((Object)(object)main == (Object)null)
			{
				arrow.SetActive(false);
				((Component)distanceText).gameObject.SetActive(false);
				return;
			}
			arrow.SetActive(true);
			((Component)distanceText).gameObject.SetActive(true);
			Vector3 val = main.WorldToScreenPoint(targetWorldPos);
			if (val.z < 0f)
			{
				val.x = (float)Screen.width - val.x;
				val.y = (float)Screen.height - val.y;
				val += (val - new Vector3((float)Screen.width * 0.5f, (float)Screen.height * 0.5f, 0f)) * 1000f;
			}
			Vector2 val2 = default(Vector2);
			((Vector2)(ref val2))..ctor((float)Screen.width * 0.5f, (float)Screen.height * 0.5f);
			Vector2 val3 = default(Vector2);
			((Vector2)(ref val3))..ctor(val.x, val.y);
			float value = Plugin.ConfigArrowEdgeMargin.Value;
			Vector2 val4 = default(Vector2);
			((Vector2)(ref val4))..ctor(value, value);
			Vector2 val5 = default(Vector2);
			((Vector2)(ref val5))..ctor((float)Screen.width - value, (float)Screen.height - value);
			bool num = val.z > 0f && val3.x >= val4.x && val3.x <= val5.x && val3.y >= val4.y && val3.y <= val5.y;
			Vector2 val6 = val3 - val2;
			if (((Vector2)(ref val6)).sqrMagnitude < 0.0001f)
			{
				val6 = Vector2.up;
			}
			Vector2 val7 = default(Vector2);
			if (num)
			{
				((Vector2)(ref val7))..ctor(val3.x, Mathf.Min(val3.y + 40f, val5.y));
			}
			else
			{
				val7 = ClampToScreenEdge(val2, val6, val4, val5);
			}
			arrowRect.anchoredPosition = val7;
			float num2 = Mathf.Atan2(val6.y, val6.x) * 57.29578f - 90f;
			((Transform)arrowRect).localRotation = Quaternion.Euler(0f, 0f, num2);
			float num3 = 0f;
			if ((Object)(object)Player.m_localPlayer != (Object)null)
			{
				num3 = Vector3.Distance(((Component)Player.m_localPlayer).transform.position, targetWorldPos);
			}
			distanceText.text = $"{Mathf.RoundToInt(num3)}m";
			distanceRect.anchoredPosition = new Vector2(val7.x, val7.y - 38f);
		}

		private static Vector2 ClampToScreenEdge(Vector2 center, Vector2 direction, Vector2 min, Vector2 max)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: 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_0029: 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)
			//IL_0036: 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_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ae: 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_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			Vector2 normalized = ((Vector2)(ref direction)).normalized;
			float num = ((normalized.x > 0f) ? ((max.x - center.x) / normalized.x) : ((normalized.x < 0f) ? ((min.x - center.x) / normalized.x) : float.MaxValue));
			float num2 = ((normalized.y > 0f) ? ((max.y - center.y) / normalized.y) : ((normalized.y < 0f) ? ((min.y - center.y) / normalized.y) : float.MaxValue));
			float num3 = Mathf.Min(num, num2);
			return center + normalized * num3;
		}

		public void Destroy()
		{
			if ((Object)(object)canvasRoot != (Object)null)
			{
				Object.Destroy((Object)(object)canvasRoot);
			}
			canvasRoot = null;
			arrow = null;
			arrowRect = null;
			distanceText = null;
			distanceRect = null;
		}
	}
	public class LeaderboardUI
	{
		private GameObject canvasRoot;

		private GameObject panel;

		private Text titleText;

		private Text clockText;

		private Text statusText;

		private Transform rowList;

		private bool listDirty = true;

		private readonly List<Text> rowTexts = new List<Text>();

		private List<Racer> rowRacers;

		private List<Racer> dummyRacers;

		public const string PreviewLocalKey = "#preview-you";

		private string activeLocalKey;

		private bool PanelExists
		{
			get
			{
				if ((Object)(object)panel != (Object)null)
				{
					return Object.op_Implicit((Object)(object)panel);
				}
				return false;
			}
		}

		public bool TestMode { get; private set; }

		public void SetTestData(List<Racer> racers)
		{
			dummyRacers = racers;
			TestMode = racers != null;
			listDirty = true;
		}

		public void MarkDirty()
		{
			listDirty = true;
		}

		private void CreatePanel()
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Expected O, but got Unknown
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Expected O, but got Unknown
			//IL_014b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: 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_0194: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ff: Expected O, but got Unknown
			if ((Object)(object)canvasRoot == (Object)null)
			{
				canvasRoot = new GameObject("BoatRaceCanvasRoot", new Type[4]
				{
					typeof(RectTransform),
					typeof(Canvas),
					typeof(CanvasScaler),
					typeof(GraphicRaycaster)
				});
				Object.DontDestroyOnLoad((Object)(object)canvasRoot);
				Canvas component = canvasRoot.GetComponent<Canvas>();
				component.renderMode = (RenderMode)0;
				component.sortingOrder = 100;
				CanvasScaler component2 = canvasRoot.GetComponent<CanvasScaler>();
				component2.uiScaleMode = (ScaleMode)1;
				component2.referenceResolution = new Vector2(1920f, 1080f);
				component2.screenMatchMode = (ScreenMatchMode)0;
				component2.matchWidthOrHeight = 0.5f;
			}
			if ((Object)(object)panel != (Object)null)
			{
				Object.Destroy((Object)(object)panel);
				panel = null;
			}
			rowTexts.Clear();
			rowRacers = null;
			panel = new GameObject("BoatRacePanel", new Type[2]
			{
				typeof(RectTransform),
				typeof(Image)
			});
			panel.transform.SetParent(canvasRoot.transform, false);
			RectTransform component3 = panel.GetComponent<RectTransform>();
			component3.anchorMin = new Vector2(0f, 1f);
			component3.anchorMax = new Vector2(0f, 1f);
			component3.pivot = new Vector2(0f, 1f);
			component3.anchoredPosition = new Vector2(Plugin.ConfigPositionX.Value, Plugin.ConfigPositionY.Value);
			component3.sizeDelta = new Vector2(280f, 0f);
			Image component4 = panel.GetComponent<Image>();
			component4.sprite = null;
			((Graphic)component4).color = new Color(0f, 0f, 0f, 0.6f);
			VerticalLayoutGroup obj = panel.AddComponent<VerticalLayoutGroup>();
			((LayoutGroup)obj).padding = new RectOffset(10, 10, 8, 10);
			((HorizontalOrVerticalLayoutGroup)obj).spacing = 4f;
			((LayoutGroup)obj).childAlignment = (TextAnchor)0;
			((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true;
			((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = true;
			((HorizontalOrVerticalLayoutGroup)obj).childForceExpandWidth = true;
			((HorizontalOrVerticalLayoutGroup)obj).childForceExpandHeight = false;
			ContentSizeFitter obj2 = panel.AddComponent<ContentSizeFitter>();
			obj2.horizontalFit = (FitMode)0;
			obj2.verticalFit = (FitMode)2;
			Font font = GetFont();
			titleText = CreateText("Title", panel.transform, "Boat Race", 15, (FontStyle)1, font, (TextAnchor)4);
			clockText = CreateText("Clock", panel.transform, "00:00", 12, (FontStyle)0, font, (TextAnchor)0);
			statusText = CreateText("Status", panel.transform, "", 12, (FontStyle)1, font, (TextAnchor)0);
			rowList = CreateList("Rows", panel.transform);
			listDirty = true;
		}

		private static Font GetFont()
		{
			if (!((Object)(object)GUIManager.Instance.AveriaSerifBold != (Object)null))
			{
				return GUIManager.Instance.AveriaSerif;
			}
			return GUIManager.Instance.AveriaSerifBold;
		}

		private static Text CreateText(string name, Transform parent, string text, int fontSize, FontStyle style, Font font, TextAnchor alignment = (TextAnchor)0)
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Expected O, but got Unknown
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject(name, new Type[3]
			{
				typeof(RectTransform),
				typeof(Text),
				typeof(ContentSizeFitter)
			});
			val.transform.SetParent(parent, false);
			Text component = val.GetComponent<Text>();
			component.text = text;
			component.font = font;
			component.fontSize = fontSize;
			component.fontStyle = style;
			((Graphic)component).color = Color.white;
			component.alignment = alignment;
			component.supportRichText = true;
			component.horizontalOverflow = (HorizontalWrapMode)0;
			component.verticalOverflow = (VerticalWrapMode)0;
			ContentSizeFitter component2 = val.GetComponent<ContentSizeFitter>();
			component2.horizontalFit = (FitMode)0;
			component2.verticalFit = (FitMode)2;
			return component;
		}

		private static Transform CreateList(string name, Transform parent)
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject(name, new Type[3]
			{
				typeof(RectTransform),
				typeof(VerticalLayoutGroup),
				typeof(ContentSizeFitter)
			});
			val.transform.SetParent(parent, false);
			VerticalLayoutGroup component = val.GetComponent<VerticalLayoutGroup>();
			((HorizontalOrVerticalLayoutGroup)component).spacing = 2f;
			((LayoutGroup)component).childAlignment = (TextAnchor)0;
			((HorizontalOrVerticalLayoutGroup)component).childControlWidth = true;
			((HorizontalOrVerticalLayoutGroup)component).childControlHeight = true;
			((HorizontalOrVerticalLayoutGroup)component).childForceExpandWidth = true;
			((HorizontalOrVerticalLayoutGroup)component).childForceExpandHeight = false;
			ContentSizeFitter component2 = val.GetComponent<ContentSizeFitter>();
			component2.horizontalFit = (FitMode)0;
			component2.verticalFit = (FitMode)2;
			return val.transform;
		}

		public void SetPosition(float x, float y)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			if (PanelExists)
			{
				panel.GetComponent<RectTransform>().anchoredPosition = new Vector2(x, y);
			}
		}

		public void UpdateFrame()
		{
			if (!PanelExists)
			{
				if (GUIManager.Instance == null)
				{
					return;
				}
				CreatePanel();
				if (!PanelExists)
				{
					return;
				}
			}
			RaceManager instance = RaceManager.Instance;
			bool flag = instance.Phase != RacePhase.Idle && instance.Racers.Count > 0;
			bool flag2 = flag || TestMode;
			panel.SetActive(flag2);
			if (flag2)
			{
				if (TestMode && !flag)
				{
					clockText.text = "Elapsed  02:34";
				}
				else if (instance.Phase == RacePhase.Countdown)
				{
					clockText.text = $"Starting in {Mathf.CeilToInt(instance.CountdownRemaining)}...";
					listDirty = true;
				}
				else
				{
					clockText.text = ((instance.Phase == RacePhase.Ended) ? ("Final  " + RaceManager.FormatTime(instance.ElapsedSeconds)) : ("Elapsed  " + RaceManager.FormatTime(instance.ElapsedSeconds)));
				}
				titleText.text = ((TestMode && !flag) ? "Boat Race (preview)" : "Boat Race");
				string text = BuildLocalStatus(instance, flag);
				statusText.text = text;
				if (((Component)statusText).gameObject.activeSelf != text.Length > 0)
				{
					((Component)statusText).gameObject.SetActive(text.Length > 0);
					listDirty = true;
				}
				bool flag3 = TestMode && !flag;
				activeLocalKey = (flag3 ? "#preview-you" : instance.LocalKey);
				if (listDirty)
				{
					listDirty = false;
					List<Racer> rows = (flag3 ? dummyRacers : instance.GetLeaderboard());
					RebuildRows(rows, instance);
					LayoutRebuilder.ForceRebuildLayoutImmediate(panel.GetComponent<RectTransform>());
				}
				else
				{
					RefreshRowText(instance);
				}
			}
		}

		private static string BuildLocalStatus(RaceManager race, bool raceVisible)
		{
			if (!raceVisible)
			{
				return string.Empty;
			}
			Racer localRacer = race.GetLocalRacer();
			if (localRacer == null)
			{
				if (race.Phase != RacePhase.Countdown && race.Phase != RacePhase.Racing)
				{
					return string.Empty;
				}
				return "<color=#AFC6E0>Spectating -- you are not in this race.</color>";
			}
			return localRacer.Status switch
			{
				RacerStatus.Dnf => "<color=#FF8A80>DNF -- you left the world mid-race.</color>\n<color=#AFC6E0>Disconnecting ends your run; your place is not restored on rejoining. Wait for the next race.</color>", 
				RacerStatus.Finished => $"<color=#8CFF8C>You finished #{localRacer.FinishPlace} -- {RaceManager.FormatTime(localRacer.FinishTime)}.</color>", 
				_ => string.Empty, 
			};
		}

		private static float EffectiveTime(Racer r, float elapsed)
		{
			if (r.Status != RacerStatus.Finished)
			{
				return elapsed + r.PenaltySeconds;
			}
			return r.FinishTime;
		}

		private void RebuildRows(List<Racer> rows, RaceManager race)
		{
			for (int num = rowList.childCount - 1; num >= 0; num--)
			{
				Object.Destroy((Object)(object)((Component)rowList.GetChild(num)).gameObject);
			}
			rowTexts.Clear();
			rowRacers = null;
			Font font = GetFont();
			if (rows == null || rows.Count == 0)
			{
				CreateText("Empty", rowList, "No racers", 12, (FontStyle)0, font, (TextAnchor)0);
				return;
			}
			rowRacers = rows;
			float elapsed = EffectiveElapsed(race);
			int gateCount = EffectiveGateCount(race);
			float leaderTime = EffectiveTime(rows[0], elapsed);
			string localKey = activeLocalKey;
			for (int i = 0; i < rows.Count; i++)
			{
				string text = FormatRow(i, rows[i], elapsed, gateCount, leaderTime, localKey);
				rowTexts.Add(CreateText($"Row{i + 1}", rowList, text, 12, (FontStyle)0, font, (TextAnchor)0));
			}
		}

		private void RefreshRowText(RaceManager race)
		{
			if (rowRacers == null || rowTexts.Count != rowRacers.Count)
			{
				return;
			}
			float elapsed = EffectiveElapsed(race);
			int gateCount = EffectiveGateCount(race);
			float leaderTime = EffectiveTime(rowRacers[0], elapsed);
			string localKey = activeLocalKey;
			for (int i = 0; i < rowRacers.Count; i++)
			{
				Text val = rowTexts[i];
				if (!((Object)(object)val == (Object)null))
				{
					val.text = FormatRow(i, rowRacers[i], elapsed, gateCount, leaderTime, localKey);
				}
			}
		}

		private static string FormatRow(int index, Racer r, float elapsed, int gateCount, float leaderTime, string localKey)
		{
			if (r.Status == RacerStatus.Dnf)
			{
				string text = $"{index + 1}. {r.PlayerName}  DNF  (gate {Mathf.Min(r.NextGateIndex, gateCount)}/{gateCount})";
				if (!IsLocal(r, localKey))
				{
					return "<color=#8A8A8A>" + text + "</color>";
				}
				return "<color=#C0A060>" + text + "</color>";
			}
			string text2;
			if (r.Status == RacerStatus.Finished)
			{
				text2 = "Finished -- " + RaceManager.FormatTime(r.FinishTime);
			}
			else if (index == 0)
			{
				text2 = RaceManager.FormatTime(EffectiveTime(r, elapsed));
			}
			else
			{
				float seconds = EffectiveTime(r, elapsed) - leaderTime;
				text2 = "+" + RaceManager.FormatTime(seconds);
			}
			string text3 = ((r.Status == RacerStatus.Finished) ? "" : $"  (gate {Mathf.Min(r.NextGateIndex, gateCount)}/{gateCount})");
			string text4 = $"{index + 1}. {r.PlayerName}  {text2}{text3}";
			if (IsLocal(r, localKey))
			{
				text4 = "<color=#FFD24A>" + text4 + "</color>";
			}
			return text4;
		}

		private static bool IsLocal(Racer r, string localKey)
		{
			if (!string.IsNullOrEmpty(localKey))
			{
				return string.Equals(r.Key, localKey, StringComparison.Ordinal);
			}
			return false;
		}

		private float EffectiveElapsed(RaceManager race)
		{
			if (!TestMode || race.Phase != RacePhase.Idle)
			{
				return race.ElapsedSeconds;
			}
			return 154f;
		}

		private static int EffectiveGateCount(RaceManager race)
		{
			if (race.GateCount <= 0)
			{
				return 7;
			}
			return race.GateCount;
		}

		public void Destroy()
		{
			if ((Object)(object)panel != (Object)null)
			{
				Object.Destroy((Object)(object)panel);
			}
			panel = null;
			rowTexts.Clear();
			rowRacers = null;
		}
	}
	public class RaceHud : MonoBehaviour
	{
		private bool testMode;

		public LeaderboardUI Leaderboard { get; private set; }

		public GateMarkerManager Markers { get; private set; }

		public MapPinManager MapPins { get; private set; }

		public DirectionArrowUI Arrow { get; private set; }

		public CountdownDisplay Countdown { get; private set; }

		private void Awake()
		{
			Leaderboard = new LeaderboardUI();
			Markers = new GateMarkerManager();
			MapPins = new MapPinManager();
			Arrow = new DirectionArrowUI();
			Countdown = new CountdownDisplay();
			RaceManager.Instance.OnStateChanged += Leaderboard.MarkDirty;
			RaceManager.Instance.OnRemoteStateApplied += Leaderboard.MarkDirty;
			Logger.LogInfo((object)"[BoatRace] RaceHud created (client visuals active).");
		}

		public bool ToggleTestMode()
		{
			testMode = !testMode;
			Logger.LogInfo((object)$"[BoatRace] Test UI mode: {testMode}");
			Leaderboard.SetTestData(testMode ? BuildDummyRacers() : null);
			return testMode;
		}

		public void ReloadLocalGates()
		{
			GateConfig.Load();
		}

		private static List<Racer> BuildDummyRacers()
		{
			return new List<Racer>
			{
				new Racer
				{
					Key = "pv1",
					PlayerName = "Sailor One",
					Status = RacerStatus.Finished,
					NextGateIndex = 8,
					FinishTime = 142f,
					FinishPlace = 1
				},
				new Racer
				{
					Key = "#preview-you",
					PlayerName = "You",
					Status = RacerStatus.Racing,
					NextGateIndex = 4,
					PenaltySeconds = 15f
				},
				new Racer
				{
					Key = "pv3",
					PlayerName = "Sailor Three",
					Status = RacerStatus.Racing,
					NextGateIndex = 3
				},
				new Racer
				{
					Key = "pv4",
					PlayerName = "Sailor Four",
					Status = RacerStatus.Racing,
					NextGateIndex = 2,
					PenaltySeconds = 30f
				},
				new Racer
				{
					Key = "pv5",
					PlayerName = "Sailor Five",
					Status = RacerStatus.Dnf,
					NextGateIndex = 2
				}
			};
		}

		private void Update()
		{
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			RaceManager instance = RaceManager.Instance;
			if (instance != null)
			{
				Leaderboard.UpdateFrame();
				Countdown.UpdateFrame(instance);
				bool num = instance.Phase == RacePhase.Countdown || instance.Phase == RacePhase.Racing;
				Racer localRacer = instance.GetLocalRacer();
				bool flag = localRacer != null && localRacer.Status == RacerStatus.Racing;
				int num2;
				bool flag2;
				if (num && flag)
				{
					num2 = localRacer.NextGateIndex;
					flag2 = true;
				}
				else if (testMode)
				{
					num2 = 1;
					flag2 = GateConfig.IsValid;
				}
				else
				{
					num2 = -1;
					flag2 = false;
				}
				bool flag3 = (num || testMode) && GateConfig.IsValid;
				Markers.SetVisible(flag3);
				if (flag3)
				{
					Markers.UpdateFrame(num2);
				}
				MapPins.SetEnabled(flag2);
				MapPins.UpdateFrame(num2);
				Arrow.SetVisible(flag2);
				if (flag2)
				{
					Gate gate = GateConfig.GetGate(num2);
					Arrow.UpdateFrame(gate != null, gate?.Center ?? Vector3.zero);
				}
				else
				{
					Arrow.UpdateFrame(hasTarget: false, Vector3.zero);
				}
			}
		}

		public void ResetForWorldExit()
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			testMode = false;
			if (Markers != null)
			{
				Markers.NotifyWorldExit();
			}
			if (MapPins != null)
			{
				MapPins.SetEnabled(value: false);
				MapPins.RemovePin();
			}
			if (Arrow != null)
			{
				Arrow.SetVisible(value: false);
				Arrow.UpdateFrame(hasTarget: false, Vector3.zero);
			}
			if (Countdown != null)
			{
				Countdown.NotifyWorldExit();
			}
			if (Leaderboard != null)
			{
				Leaderboard.SetTestData(null);
				Leaderboard.MarkDirty();
			}
			Logger.LogInfo((object)"[BoatRace] HUD reset for world exit -- markers, pin, arrow, countdown and leaderboard cleared.");
		}

		private void OnDestroy()
		{
			if (RaceManager.Instance != null)
			{
				RaceManager.Instance.OnStateChanged -= Leaderboard.MarkDirty;
				RaceManager.Instance.OnRemoteStateApplied -= Leaderboard.MarkDirty;
			}
			if (Markers != null)
			{
				Markers.Dispose();
			}
			if (MapPins != null)
			{
				MapPins.RemovePin();
			}
			if (Arrow != null)
			{
				Arrow.Destroy();
			}
			if (Leaderboard != null)
			{
				Leaderboard.Destroy();
			}
		}
	}
}
namespace BoatRace.Race
{
	public static class GateDetector
	{
		public static bool CrossesGate(Gate gate, Vector3 from, Vector3 to, float spanMargin)
		{
			//IL_0001: 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_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			float num = SignedDistance(gate, from);
			float num2 = SignedDistance(gate, to);
			if (num == 0f && num2 == 0f)
			{
				return false;
			}
			if (num > 0f == num2 > 0f)
			{
				return false;
			}
			float num3 = num - num2;
			if (Mathf.Abs(num3) < 1E-06f)
			{
				return false;
			}
			float num4 = num / num3;
			if (num4 < 0f || num4 > 1f)
			{
				return false;
			}
			Vector3 val = Vector3.Lerp(from, to, num4) - gate.PointA;
			val.y = 0f;
			float num5 = Vector3.Dot(val, gate.AxisNormalized);
			if (num5 >= 0f - spanMargin)
			{
				return num5 <= gate.Length + spanMargin;
			}
			return false;
		}

		public static float SignedDistance(Gate gate, Vector3 point)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: 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 = point - gate.PointA;
			val.y = 0f;
			return Vector3.Dot(val, gate.PlaneNormal);
		}
	}
	public enum RacePhase
	{
		Idle,
		Countdown,
		Racing,
		Ended
	}
	public enum PositionLookup
	{
		Ok,
		NoZNetOrZdoMan,
		NoPeer,
		CharacterIdNone,
		NoZdo
	}
	public class RaceManager
	{
		public class ConnectedPlayer
		{
			public long PeerId;

			public string Name;

			public string Identity;
		}

		private readonly Dictionary<string, string> identityKeys = new Dictionary<string, string>();

		private int nextIdentityKey;

		public const string HostIdentity = "#host";

		private int nextFinishPlace = 1;

		private float detectionAccumulator;

		private bool applyingRemoteState;

		public static RaceManager Instance { get; private set; }

		public RacePhase Phase { get; private set; }

		public float ElapsedSeconds { get; private set; }

		public float CountdownRemaining { get; private set; }

		public Dictionary<string, Racer> Racers { get; } = new Dictionary<string, Racer>();

		public string LocalKey { get; private set; }

		public int GateCount => GateConfig.Gates.Count;

		public bool IsActive
		{
			get
			{
				if (Phase != RacePhase.Countdown)
				{
					return Phase == RacePhase.Racing;
				}
				return true;
			}
		}

		public bool IsApplyingRemoteState => applyingRemoteState;

		public event Action OnStateChanged;

		public event Action<Racer> OnRacerFinished;

		public event Action<Racer, int> OnPenaltyApplied;

		public event Action OnRemoteStateApplied;

		public RaceManager()
		{
			Instance = this;
		}

		public bool StartRace(out string failureReason)
		{
			if (!GateConfig.IsValid)
			{
				failureReason = "Gate config is invalid or missing (BoatRace_Gates.json). Fix it and run boatrace_reload.";
				return false;
			}
			if (IsActive)
			{
				failureReason = "A race is already in progress.";
				return false;
			}
			Racers.Clear();
			nextFinishPlace = 1;
			ElapsedSeconds = 0f;
			detectionAccumulator = 0f;
			foreach (ConnectedPlayer item in EnumerateConnectedPlayers())
			{
				string opaqueKey = GetOpaqueKey(item.Identity);
				if (string.IsNullOrEmpty(opaqueKey))
				{
					Logger.LogWarning((object)$"[BoatRace] Skipping '{item.Name}' (peer {item.PeerId}) -- no durable identity could be resolved, so their progress could not survive a rejoin.");
					continue;
				}
				Racers[opaqueKey] = new Racer
				{
					Key = opaqueKey,
					PeerId = item.PeerId,
					PlayerName = item.Name,
					Status = RacerStatus.Racing,
					NextGateIndex = 1
				};
				if (item.Identity == "#host")
				{
					SetLocalKey(opaqueKey);
				}
			}
			if (Racers.Count == 0)
			{
				failureReason = "No connected players to race.";
				return false;
			}
			Phase = RacePhase.Countdown;
			CountdownRemaining = Plugin.ConfigCountdownSeconds.Value;
			Logger.LogInfo((object)$"[BoatRace] Race starting: {Racers.Count} racer(s), {GateCount} gates, {CountdownRemaining:0}s countdown.");
			foreach (Racer value in Racers.Values)
			{
				Logger.LogInfo((object)$"[BoatRace]   racer key={value.Key} peer={value.PeerId} name={value.PlayerName}");
			}
			RaiseStateChanged();
			failureReason = null;
			return true;
		}

		public void EndRace(string reason)
		{
			if (Phase == RacePhase.Idle || Phase == RacePhase.Ended)
			{
				Logger.LogInfo((object)$"[BoatRace] EndRace ignored -- phase is {Phase}.");
				return;
			}
			foreach (Racer item in Racers.Values.ToList())
			{
				if (item.Status == RacerStatus.Racing)
				{
					item.Status = RacerStatus.Dnf;
				}
			}
			Phase = RacePhase.Ended;
			Logger.LogInfo((object)("[BoatRace] Race ended (" + reason + ")."));
			RaiseStateChanged();
		}

		public void ResetRace()
		{
			Racers.Clear();
			Phase = RacePhase.Idle;
			ElapsedSeconds = 0f;
			CountdownRemaining = 0f;
			nextFinishPlace = 1;
			detectionAccumulator = 0f;
			Logger.LogInfo((object)"[BoatRace] Race state reset.");
			RaiseStateChanged();
		}

		public string GetOpaqueKey(string durableIdentity)
		{
			if (string.IsNullOrEmpty(durableIdentity))
			{
				return null;
			}
			if (!identityKeys.TryGetValue(durableIdentity, out var value))
			{
				int num = ++nextIdentityKey;
				value = "p" + num.ToString(CultureInfo.InvariantCulture);
				identityKeys[durableIdentity] = value;
			}
			return value;
		}

		public bool RebindPeer(string key, long newPeerId, string playerName)
		{
			if (string.IsNullOrEmpty(key) || !Racers.TryGetValue(key, out var value))
			{
				return false;
			}
			long peerId = value.PeerId;
			value.PeerId = newPeerId;
			if (!string.IsNullOrEmpty(playerName))
			{
				value.PlayerName = playerName;
			}
			value.HasLastPosition = false;
			Logger.LogInfo((object)$"[BoatRace] Rebound racer key={key} name='{value.PlayerName}' from peer {peerId} to peer {newPeerId} (rejoin). Status stays {value.Status}.");
			return true;
		}

		public bool TryGetRacerByPeerId(long peerId, out Racer racer)
		{
			foreach (Racer value in Racers.Values)
			{
				if (value.PeerId == peerId)
				{
					racer = value;
					return true;
				}
			}
			racer = null;
			return false;
		}

		public void MarkDnf(long peerId)
		{
			if (TryGetRacerByPeerId(peerId, out var racer) && racer.Status == RacerStatus.Racing)
			{
				racer.Status = RacerStatus.Dnf;
				Logger.LogInfo((object)$"[BoatRace] {racer.PlayerName} (peer {peerId}) disconnected mid-race -- marked DNF.");
				RaiseStateChanged();
				if (AllRacersSettled())
				{
					EndRace("all racers finished or DNF");
				}
			}
		}

		public void Tick(float deltaTime, bool isServer)
		{
			if (Phase == RacePhase.Countdown)
			{
				CountdownRemaining -= deltaTime;
				if (CountdownRemaining <= 0f)
				{
					CountdownRemaining = 0f;
					if (isServer)
					{
						Phase = RacePhase.Racing;
						ElapsedSeconds = 0f;
						Logger.LogInfo((object)"[BoatRace] Countdown complete -- race is live.");
						RaiseStateChanged();
					}
				}
			}
			else
			{
				if (Phase != RacePhase.Racing)
				{
					return;
				}
				ElapsedSeconds += deltaTime;
				if (isServer)
				{
					detectionAccumulator += deltaTime;
					float num = Mathf.Max(0.05f, Plugin.ConfigDetectionInterval.Value);
					if (!(detectionAccumulator < num))
					{
						float sampleDelta = detectionAccumulator;
						detectionAccumulator = 0f;
						SampleAndDetect(sampleDelta);
					}
				}
			}
		}

		private void SampleAndDetect(float sampleDelta)
		{
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: 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_010f: 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_0116: Unknown result type (might be due to invalid IL or missing references)
			float value = Plugin.ConfigGateSpanMargin.Value;
			float value2 = Plugin.ConfigMaxSampleJump.Value;
			foreach (Racer item in Racers.Values.ToList())
			{
				if (item.Status != RacerStatus.Racing)
				{
					continue;
				}
				if (!TryGetPlayerPosition(item.PeerId, out var position, out var outcome))
				{
					if (!item.PositionLookupFailing)
					{
						item.PositionLookupFailing = true;
						Logger.LogWarning((object)$"[BoatRace] Position lookup FAILED for {item.PlayerName} (peer {item.PeerId}): {DescribeLookup(outcome)}. Gate detection is suspended for this racer until it recovers; further failures for this racer are suppressed.");
					}
					continue;
				}
				if (item.PositionLookupFailing)
				{
					item.PositionLookupFailing = false;
					Logger.LogInfo((object)$"[BoatRace] Position lookup RECOVERED for {item.PlayerName} (peer {item.PeerId}) -- gate detection resumed.");
				}
				if (!item.HasLastPosition)
				{
					item.LastPosition = position;
					item.HasLastPosition = true;
					continue;
				}
				Vector3 lastPosition = item.LastPosition;
				item.LastPosition = position;
				Vector3 val = position - lastPosition;
				if (((Vector3)(ref val)).sqrMagnitude > value2 * value2)
				{
					string playerName = item.PlayerName;
					val = position - lastPosition;
					Logger.LogDebug((object)$"[BoatRace][DIAG] {playerName} moved {((Vector3)(ref val)).magnitude:0.0}m in {sampleDelta:0.00}s -- ignoring sample as a teleport.");
				}
				else
				{
					CheckGatesForRacer(item, lastPosition, position, value);
				}
			}
		}

		private void CheckGatesForRacer(Racer racer, Vector3 from, Vector3 to, float margin)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			IReadOnlyList<Gate> gates = GateConfig.Gates;
			for (int i = 0; i < gates.Count; i++)
			{
				Gate gate = gates[i];
				if (!GateDetector.CrossesGate(gate, from, to, margin))
				{
					continue;
				}
				if (gate.Index == racer.NextGateIndex)
				{
					racer.NextGateIndex++;
					Logger.LogInfo((object)$"[BoatRace][DIAG] {racer.PlayerName} crossed gate {gate.Index} ({gate.Name}) in sequence; next gate {racer.NextGateIndex}.");
					if (racer.NextGateIndex > GateCount)
					{
						FinishRacer(racer);
						break;
					}
				}
				else
				{
					float value = Plugin.ConfigPenaltySeconds.Value;
					racer.PenaltySeconds += value;
					Logger.LogInfo((object)$"[BoatRace][DIAG] {racer.PlayerName} crossed gate {gate.Index} OUT OF SEQUENCE (expected {racer.NextGateIndex}) -- +{value:0}s penalty (total {racer.PenaltySeconds:0}s). Progress unchanged.");
					if (this.OnPenaltyApplied != null)
					{
						this.OnPenaltyApplied(racer, gate.Index);
					}
				}
				RaiseStateChanged();
			}
		}

		private void FinishRacer(Racer racer)
		{
			racer.Status = RacerStatus.Finished;
			racer.FinishTime = ElapsedSeconds + racer.PenaltySeconds;
			racer.FinishPlace = nextFinishPlace++;
			Logger.LogInfo((object)$"[BoatRace] {racer.PlayerName} FINISHED in place {racer.FinishPlace} with {FormatTime(racer.FinishTime)} (including {racer.PenaltySeconds:0}s penalties).");
			if (this.OnRacerFinished != null)
			{
				this.OnRacerFinished(racer);
			}
			RaiseStateChanged();
			if (AllRacersSettled())
			{
				EndRace("all racers finished or DNF");
			}
		}

		private bool AllRacersSettled()
		{
			foreach (Racer value in Racers.Values)
			{
				if (value.Status == RacerStatus.Racing)
				{
					return false;
				}
			}
			return true;
		}

		public static bool TryGetPlayerPosition(long peerId, out Vector3 position, out PositionLookup outcome)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: 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_004e: 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_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			position = Vector3.zero;
			if ((Object)(object)ZNet.instance == (Object)null || ZDOMan.instance == null)
			{
				outcome = PositionLookup.NoZNetOrZdoMan;
				return false;
			}
			ZDOID val;
			if (peerId == ZNet.GetUID())
			{
				val = ZNet.instance.LocalPlayerCharacterID;
			}
			else
			{
				ZNetPeer peer = ZNet.instance.GetPeer(peerId);
				if (peer == null)
				{
					outcome = PositionLookup.NoPeer;
					return false;
				}
				val = peer.m_characterID;
			}
			if (((ZDOID)(ref val)).IsNone())
			{
				outcome = PositionLookup.CharacterIdNone;
				return false;
			}
			ZDO zDO = ZDOMan.instance.GetZDO(val);
			if (zDO == null)
			{
				outcome = PositionLookup.NoZdo;
				return false;
			}
			position = zDO.GetPosition();
			outcome = PositionLookup.Ok;
			return true;
		}

		public static string DescribeLookup(PositionLookup outcome)
		{
			return outcome switch
			{
				PositionLookup.Ok => "ok", 
				PositionLookup.NoZNetOrZdoMan => "ZNet.instance or ZDOMan.instance is null (not in a world)", 
				PositionLookup.NoPeer => "ZNet.GetPeer(uid) returned null (peer disconnected or unknown)", 
				PositionLookup.CharacterIdNone => "m_characterID is ZDOID.None (character select, or mid-respawn)", 
				PositionLookup.NoZdo => "ZDOMan.GetZDO returned null (character ZDO not replicated to the server yet)", 
				_ => "unknown", 
			};
		}

		public static List<ConnectedPlayer> EnumerateConnectedPlayers()
		{
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			List<ConnectedPlayer> list = new List<ConnectedPlayer>();
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return list;
			}
			foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers())
			{
				if (connectedPeer != null && connectedPeer.IsReady())
				{
					if (connectedPeer.m_socket == null)
					{
						Logger.LogWarning((object)$"[BoatRace] Peer {connectedPeer.m_uid} ('{connectedPeer.m_playerName}') has no socket -- cannot resolve a durable identity, skipping enrolment.");
						continue;
					}
					list.Add(new ConnectedPlayer
					{
						PeerId = connectedPeer.m_uid,
						Name = (string.IsNullOrEmpty(connectedPeer.m_playerName) ? "Unknown" : connectedPeer.m_playerName),
						Identity = connectedPeer.m_socket.GetHostName()
					});
				}
			}
			if (ZNet.instance.IsServer())
			{
				ZDOID localPlayerCharacterID = ZNet.instance.LocalPlayerCharacterID;
				if (!((ZDOID)(ref localPlayerCharacterID)).IsNone())
				{
					string name = "Host";
					if ((Object)(object)Player.m_localPlayer != (Object)null)
					{
						name = Player.m_localPlayer.GetPlayerName();
					}
					list.Add(new ConnectedPlayer
					{
						PeerId = ZNet.GetUID(),
						Name = name,
						Identity = "#host"
					});
				}
			}
			return list;
		}

		public void ApplyRemoteState(RacePhase phase, float elapsed, float countdown, List<Racer> racers)
		{
			applyingRemoteState = true;
			try
			{
				Phase = phase;
				ElapsedSeconds = elapsed;
				CountdownRemaining = countdown;
				Racers.Clear();
				foreach (Racer racer in racers)
				{
					if (!string.IsNullOrEmpty(racer.Key))
					{
						Racers[racer.Key] = racer;
					}
				}
			}
			finally
			{
				applyingRemoteState = false;
			}
			if (this.OnRemoteStateApplied != null)
			{
				this.OnRemoteStateApplied();
			}
		}

		public void ClearForWorldExit()
		{
			bool num = Phase != RacePhase.Idle || Racers.Count > 0;
			Phase = RacePhase.Idle;
			ElapsedSeconds = 0f;
			CountdownRemaining = 0f;
			Racers.Clear();
			nextFinishPlace = 1;
			detectionAccumulator = 0f;
			if (num)
			{
				Logger.LogInfo((object)"[BoatRace] Left the world -- cleared local race state so a rejoin starts clean.");
			}
			if (this.OnRemoteStateApplied != null)
			{
				this.OnRemoteStateApplied();
			}
		}

		public void SetLocalKey(string key)
		{
			if (!string.Equals(LocalKey, key, StringComparison.Ordinal))
			{
				LocalKey = key;
				Logger.LogInfo((object)("[BoatRace] This machine's race identity is " + key + " (assigned by the server; survives a disconnect and rejoin)."));
				if (this.OnRemoteStateApplied != null)
				{
					this.OnRemoteStateApplied();
				}
			}
		}

		public Racer GetLocalRacer()
		{
			if (string.IsNullOrEmpty(LocalKey))
			{
				return null;
			}
			if (!Racers.TryGetValue(LocalKey, out var value))
			{
				return null;
			}
			return value;
		}

		public Gate GetLocalNextGate()
		{
			Racer localRacer = GetLocalRacer();
			if (localRacer == null || localRacer.Status != RacerStatus.Racing)
			{
				return null;
			}
			return GateConfig.GetGate(localRacer.NextGateIndex);
		}

		public List<Racer> GetLeaderboard()
		{
			List<Racer> list = new List<Racer>();
			List<Racer> list2 = new List<Racer>();
			List<Racer> list3 = new List<Racer>();
			foreach (Racer value in Racers.Values)
			{
				if (value.Status == RacerStatus.Finished)
				{
					list.Add(value);
				}
				else if (value.Status == RacerStatus.Racing)
				{
					list2.Add(value);
				}
				else
				{
					list3.Add(value);
				}
			}
			list3.Sort((Racer a, Racer b) => string.Compare(a.PlayerName, b.PlayerName, StringComparison.Ordinal));
			list.Sort((Racer a, Racer b) => a.FinishPlace.CompareTo(b.FinishPlace));
			list2.Sort(delegate(Racer a, Racer b)
			{
				int num = b.GatesCompleted.CompareTo(a.GatesCompleted);
				if (num != 0)
				{
					return num;
				}
				int num2 = a.PenaltySeconds.CompareTo(b.PenaltySeconds);
				return (num2 != 0) ? num2 : string.Compare(a.PlayerName, b.PlayerName, StringComparison.Ordinal);
			});
			list.AddRange(list2);
			list.AddRange(list3);
			return list;
		}

		public static string FormatTime(float seconds)
		{
			if (seconds < 0f)
			{
				seconds = 0f;
			}
			int num = Mathf.FloorToInt(seconds);
			return (num / 60).ToString("00") + ":" + (num % 60).ToString("00");
		}

		private void RaiseStateChanged()
		{
			if (!applyingRemoteState && this.OnStateChanged != null)
			{
				this.OnStateChanged();
			}
		}
	}
	public enum RacerStatus
	{
		Racing,
		Finished,
		Dnf
	}
	public class Racer
	{
		public string Key;

		public long PeerId;

		public string PlayerName;

		public RacerStatus Status;

		public int NextGateIndex = 1;

		public float PenaltySeconds;

		public float FinishTime;

		public int FinishPlace;

		public Vector3 LastPosition;

		public bool HasLastPosition;

		public bool PositionLookupFailing;

		public int GatesCompleted => NextGateIndex - 1;
	}
	public class ServerTicker : MonoBehaviour
	{
		private float syncTimer;

		private float peerScanTimer;

		private float inWorldSeconds;

		private bool loggedRegistrationTimeout;

		private bool loggedWaitingForWorld;

		private bool fingerprintReported;

		private bool wasInWorld;

		private readonly HashSet<long> knownPeers = new HashSet<long>();

		private void Start()
		{
			Logger.LogInfo((object)"[BoatRace] ServerTicker started -- race tick active on all machines; authoritative branches gated on IsServer.");
		}

		private void Update()
		{
			RetryRpcRegistration();
			ReportGateFingerprintOnce();
			RaceManager instance = RaceManager.Instance;
			if (instance == null)
			{
				return;
			}
			TrackWorldTransition(instance);
			bool flag = (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();
			instance.Tick(Time.deltaTime, flag);
			if (!flag)
			{
				return;
			}
			ScanForDisconnects();
			if (!instance.IsActive)
			{
				return;
			}
			syncTimer -= Time.deltaTime;
			if (syncTimer <= 0f)
			{
				syncTimer = Mathf.Max(0.25f, Plugin.ConfigSyncInterval.Value);
				if (RpcManager.Instance != null)
				{
					RpcManager.Instance.BroadcastState();
				}
			}
		}

		private void TrackWorldTransition(RaceManager race)
		{
			bool flag = (Object)(object)ZNet.instance != (Object)null;
			if (flag == wasInWorld)
			{
				return;
			}
			wasInWorld = flag;
			if (!flag)
			{
				race.ClearForWorldExit();
				Plugin instance = Plugin.Instance;
				if ((Object)(object)instance != (Object)null && (Object)(object)instance.Hud != (Object)null)
				{
					instance.Hud.ResetForWorldExit();
				}
			}
		}

		private void RetryRpcRegistration()
		{
			if (RpcManager.Instance == null || RpcManager.Instance.Registered)
			{
				return;
			}
			RpcManager.Instance.Register();
			if (RpcManager.Instance.Registered)
			{
				Logger.LogInfo((object)"[BoatRace] ServerTicker: RpcManager registered.");
				inWorldSeconds = 0f;
				loggedRegistrationTimeout = false;
				loggedWaitingForWorld = false;
			}
			else if ((Object)(object)ZNet.instance == (Object)null)
			{
				inWorldSeconds = 0f;
				loggedRegistrationTimeout = false;
				if (!loggedWaitingForWorld)
				{
					loggedWaitingForWorld = true;
					Logger.LogInfo((object)"[BoatRace] ServerTicker: not in a world yet, deferring RPC registration (normal at the main menu).");
				}
			}
			else
			{
				loggedWaitingForWorld = false;
				inWorldSeconds += Time.deltaTime;
				if (inWorldSeconds >= 30f && !loggedRegistrationTimeout)
				{
					loggedRegistrationTimeout = true;
					Logger.LogWarning((object)"[BoatRace] ServerTicker: RpcManager STILL UNREGISTERED 30s after joining a world. Inbound RPCs will not be received on this machine.");
				}
			}
		}

		private void ReportGateFingerprintOnce()
		{
			RpcManager instance = RpcManager.Instance;
			if (instance != null)
			{
				if ((Object)(object)ZNet.instance == (Object)null || !instance.Registered)
				{
					fingerprintReported = false;
				}
				else if (!fingerprintReported)
				{
					fingerprintReported = true;
					instance.SendFingerprintToServer();
				}
			}
		}

		private void ScanForDisconnects()
		{
			peerScanTimer -= Time.deltaTime;
			if (peerScanTimer > 0f)
			{
				return;
			}
			peerScanTimer = 1f;
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			HashSet<long> hashSet = new HashSet<long>();
			foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers())
			{
				if (connectedPeer != null && connectedPeer.IsReady())
				{
					hashSet.Add(connectedPeer.m_uid);
				}
			}
			hashSet.Add(ZNet.GetUID());
			RaceManager instance = RaceManager.Instance;
			if (instance.IsActive)
			{
				List<long> list = new List<long>();
				foreach (Racer value in instance.Racers.Values)
				{
					list.Add(value.PeerId);
				}
				foreach (long item in list)
				{
					if (knownPeers.Contains(item) && !hashSet.Contains(item))
					{
						instance.MarkDnf(item);
					}
				}
			}
			knownPeers.Clear();
			foreach (long item2 in hashSet)
			{
				knownPeers.Add(item2);
			}
			if (RpcManager.Instance != null)
			{
				RpcManager.Instance.PruneFingerprints(hashSet);
			}
		}
	}
}
namespace BoatRace.Patches
{
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	public static class PeerConnectedPatch
	{
		[HarmonyPostfix]
		private static void Postfix(ZNet __instance, ZRpc rpc)
		{
			if ((Object)(object)__instance == (Object)null || !__instance.IsServer())
			{
				return;
			}
			ZNetPeer peer = PeerLookup.GetPeer(__instance, rpc);
			if (peer == null || string.IsNullOrEmpty(peer.m_playerName))
			{
				return;
			}
			if (RpcManager.Instance == null)
			{
				Logger.LogWarning((object)$"[BoatRace] Peer {peer.m_uid} ('{peer.m_playerName}') connected before RpcManager existed -- no race state pushed.");
				return;
			}
			if (peer.m_socket != null)
			{
				RpcManager.Instance.OnPeerConnected(peer.m_uid, peer.m_playerName, peer.m_socket.GetHostName());
			}
			else
			{
				Logger.LogWarning((object)$"[BoatRace] Peer {peer.m_uid} ('{peer.m_playerName}') has no socket at connect -- cannot assign a race identity; they will not see their own row.");
			}
			RpcManager.Instance.PushStateTo(peer.m_uid, peer.m_playerName);
		}
	}
	public static class PeerLookup
	{
		public static ZNetPeer GetPeer(ZNet znet, ZRpc rpc)
		{
			if ((Object)(object)znet == (Object)null || rpc == null)
			{
				return null;
			}
			List<ZNetPeer> connectedPeers = znet.GetConnectedPeers();
			if (connectedPeers == null)
			{
				return null;
			}
			for (int i = 0; i < connectedPeers.Count; i++)
			{
				ZNetPeer val = connectedPeers[i];
				if (val != null && val.m_rpc == rpc)
				{
					return val;
				}
			}
			return null;
		}
	}
}
namespace BoatRace.Networking
{
	public class RpcManager
	{
		public const string RpcSyncState = "BoatRace_SyncState";

		public const string RpcAnnounceCountdown = "BoatRace_AnnounceCountdown";

		public const string RpcAnnounceFinish = "BoatRace_AnnounceFinish";

		public const string RpcAnnounceRaceEnd = "BoatRace_AnnounceRaceEnd";

		public const string RpcCommandResult = "BoatRace_CommandResult";

		public const string RpcPenaltyNotice = "BoatRace_PenaltyNotice";

		public const string RpcAssignKey = "BoatRace_AssignKey";

		public const string RpcRequestStart = "BoatRace_RequestStart";

		public const string RpcRequestEnd = "BoatRace_RequestEnd";

		public const string RpcRequestReset = "BoatRace_RequestReset";

		public const string RpcRequestReload = "BoatRace_RequestReload";

		public const string RpcReportFingerprint = "BoatRace_ReportFingerprint";

		public const string RpcRequestDebugState = "BoatRace_RequestDebugState";

		private bool registered;

		private ZRoutedRpc registeredOn;

		private readonly Dictionary<long, string> peerFingerprints = new Dictionary<long, string>();

		public static RpcManager Instance { get; private set; }

		public bool Registered
		{
			get
			{
				if (registered)
				{
					return registeredOn == ZRoutedRpc.instance;
				}
				return false;
			}
		}

		public RpcManager()
		{
			Instance = this;
			RaceManager.Instance.OnStateChanged += OnStateChanged;
			RaceManager.Instance.OnRacerFinished += OnRacerFinished;
			RaceManager.Instance.OnPenaltyApplied += OnPenaltyApplied;
			GateConfig.OnGatesReloaded += SendFingerprintToServer;
			PrefabManager.OnVanillaPrefabsAvailable += Register;
		}

		public void Register()
		{
			if (!Registered && ZRoutedRpc.instance != null)
			{
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_SyncState", (Action<long, ZPackage>)RPC_SyncState);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_AnnounceCountdown", (Action<long, ZPackage>)RPC_AnnounceCountdown);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_AnnounceFinish", (Action<long, ZPackage>)RPC_AnnounceFinish);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_AnnounceRaceEnd", (Action<long, ZPackage>)RPC_AnnounceRaceEnd);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_CommandResult", (Action<long, ZPackage>)RPC_CommandResult);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_PenaltyNotice", (Action<long, ZPackage>)RPC_PenaltyNotice);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_AssignKey", (Action<long, ZPackage>)RPC_AssignKey);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_RequestStart", (Action<long, ZPackage>)RPC_RequestStart);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_RequestEnd", (Action<long, ZPackage>)RPC_RequestEnd);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_RequestReset", (Action<long, ZPackage>)RPC_RequestReset);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_RequestReload", (Action<long, ZPackage>)RPC_RequestReload);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_RequestDebugState", (Action<long, ZPackage>)RPC_RequestDebugState);
				ZRoutedRpc.instance.Register<ZPackage>("BoatRace_ReportFingerprint", (Action<long, ZPackage>)RPC_ReportFingerprint);
				registeredOn = ZRoutedRpc.instance;
				registered = true;
				Logger.LogInfo((object)"[BoatRace] RPCs registered.");
			}
		}

		public void SendToServer(string rpcName, ZPackage pkg)
		{
			if (ZRoutedRpc.instance == null)
			{
				Logger.LogWarning((object)("[BoatRace] SendToServer(" + rpcName + ") -- ZRoutedRpc.instance is null, dropping."));
				return;
			}
			Register();
			ZRoutedRpc.instance.InvokeRoutedRPC(rpcName, new object[1] { pkg });
		}

		private bool IsSenderAdmin(long sender)
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return false;
			}
			if (ZNet.instance.IsServer() && sender == ZNet.GetUID())
			{
				return true;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			if (peer == null)
			{
				Logger.LogWarning((object)$"[BoatRace] IsSenderAdmin: no peer for sender {sender} (server UID {ZNet.GetUID()}) -- rejecting.");
				return false;
			}
			if (peer.m_socket == null)
			{
				Logger.LogWarning((object)$"[BoatRace] IsSenderAdmin: peer for sender {sender} has a null socket (mid-teardown) -- rejecting.");
				return false;
			}
			return ZNet.instance.IsAdmin(peer.m_socket.GetHostName());
		}

		private static bool IsFromServer(long sender)
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return false;
			}
			if (ZNet.instance.IsServer())
			{
				return sender == ZNet.GetUID();
			}
			ZNetPeer serverPeer = ZNet.instance.GetServerPeer();
			if (serverPeer != null)
			{
				return sender == serverPeer.m_uid;
			}
			return false;
		}

		public void BroadcastState()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				if (Registered)
				{
					ZPackage val = new ZPackage();
					WriteState(val);
					ZRoutedRpc.instance.InvokeRoutedRPC(0L, "BoatRace_SyncState", new object[1] { val });
				}
			}
		}

		public void PushStateTo(long targetPeer, string playerName)
		{
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Expected O, but got Unknown
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			if (ZRoutedRpc.instance == null)
			{
				Logger.LogWarning((object)$"[BoatRace] Cannot push race state to peer {targetPeer} ('{playerName}') -- ZRoutedRpc.instance is null.");
				return;
			}
			Register();
			if (!Registered)
			{
				Logger.LogWarning((object)$"[BoatRace] Cannot push race state to peer {targetPeer} ('{playerName}') -- RPCs not registered yet.");
				return;
			}
			RaceManager instance = RaceManager.Instance;
			ZPackage val = new ZPackage();
			WriteState(val);
			ZRoutedRpc.instance.InvokeRoutedRPC(targetPeer, "BoatRace_SyncState", new object[1] { val });
			Logger.LogInfo((object)$"[BoatRace] Pushed race state to newly connected peer {targetPeer} ('{playerName}'): phase={instance.Phase} racers={instance.Racers.Count}.");
		}

		private static void WriteState(ZPackage pkg)
		{
			RaceManager instance = RaceManager.Instance;
			pkg.Write((int)instance.Phase);
			pkg.Write(instance.ElapsedSeconds);
			pkg.Write(instance.CountdownRemaining);
			pkg.Write(instance.Racers.Count);
			foreach (Racer value in instance.Racers.Values)
			{
				pkg.Write(value.Key ?? string.Empty);
				pkg.Write(value.PlayerName);
				pkg.Write((int)value.Status);
				pkg.Write(value.NextGateIndex);
				pkg.Write(value.PenaltySeconds);
				pkg.Write(value.FinishTime);
				pkg.Write(value.FinishPlace);
			}
		}

		public void BroadcastCountdown(float seconds, int gateCount)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(seconds);
				val.Write(gateCount);
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "BoatRace_AnnounceCountdown", new object[1] { val });
			}
		}

		private void SendResult(long targetPeer, string message)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(message);
				ZRoutedRpc.instance.InvokeRoutedRPC(targetPeer, "BoatRace_CommandResult", new object[1] { val });
			}
		}

		private void RPC_SyncState(long sender, ZPackage pkg)
		{
			if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
			{
				return;
			}
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_SyncState rejected -- sender {sender} is not the server.");
				return;
			}
			RacePhase phase = (RacePhase)pkg.ReadInt();
			float elapsed = pkg.ReadSingle();
			float countdown = pkg.ReadSingle();
			int num = pkg.ReadInt();
			List<Racer> list = new List<Racer>(num);
			for (int i = 0; i < num; i++)
			{
				list.Add(new Racer
				{
					Key = pkg.ReadString(),
					PlayerName = pkg.ReadString(),
					Status = (RacerStatus)pkg.ReadInt(),
					NextGateIndex = pkg.ReadInt(),
					PenaltySeconds = pkg.ReadSingle(),
					FinishTime = pkg.ReadSingle(),
					FinishPlace = pkg.ReadInt()
				});
			}
			RaceManager.Instance.ApplyRemoteState(phase, elapsed, countdown, list);
		}

		private void RPC_AnnounceCountdown(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_AnnounceCountdown rejected -- sender {sender} is not the server.");
				return;
			}
			float num = pkg.ReadSingle();
			int num2 = pkg.ReadInt();
			string text = $"Boat race starting! {num2} gates, {num:0}s countdown.";
			Logger.LogInfo((object)("[BoatRace] " + text));
			if ((Object)(object)Chat.instance != (Object)null)
			{
				((Terminal)Chat.instance).AddString(text);
			}
		}

		private void RPC_AnnounceFinish(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_AnnounceFinish rejected -- sender {sender} is not the server.");
				return;
			}
			string text = pkg.ReadString();
			int num = pkg.ReadInt();
			float seconds = pkg.ReadSingle();
			string text2 = ((num == 1) ? (text + " wins the boat race! (" + RaceManager.FormatTime(seconds) + ")") : $"{text} finished #{num} ({RaceManager.FormatTime(seconds)})");
			Logger.LogInfo((object)("[BoatRace] " + text2));
			if (!((Object)(object)MessageHud.instance == (Object)null))
			{
				MessageHud.instance.ShowMessage((MessageType)2, text2, 0, (Sprite)null, false, true);
				if ((Object)(object)Chat.instance != (Object)null)
				{
					((Terminal)Chat.instance).AddString(text2);
				}
			}
		}

		private void RPC_AnnounceRaceEnd(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_AnnounceRaceEnd rejected -- sender {sender} is not the server.");
				return;
			}
			string text = pkg.ReadString();
			Logger.LogInfo((object)("[BoatRace] " + text));
			if (!((Object)(object)MessageHud.instance == (Object)null))
			{
				MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
			}
		}

		private void RPC_CommandResult(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_CommandResult rejected -- sender {sender} is not the server.");
				return;
			}
			string text = pkg.ReadString();
			Logger.LogInfo((object)("[BoatRace] " + text));
			if ((Object)(object)Console.instance != (Object)null)
			{
				Console.instance.Print("[BoatRace] " + text);
			}
		}

		private void RPC_PenaltyNotice(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_PenaltyNotice rejected -- sender {sender} is not the server.");
				return;
			}
			float num = pkg.ReadSingle();
			int num2 = pkg.ReadInt();
			string text = $"+{num:0}s penalty -- gate {num2} crossed out of sequence";
			Logger.LogInfo((object)("[BoatRace] " + text));
			if (!((Object)(object)MessageHud.instance == (Object)null))
			{
				MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true);
			}
		}

		private void RPC_RequestStart(long sender, ZPackage pkg)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Logger.LogInfo((object)$"[BoatRace] RPC_RequestStart from sender {sender}");
				if (!IsSenderAdmin(sender))
				{
					Logger.LogWarning((object)$"[BoatRace] RPC_RequestStart rejected -- sender {sender} is not admin.");
					SendResult(sender, "You must be a server admin to start a race.");
					return;
				}
				if (!RaceManager.Instance.StartRace(out var failureReason))
				{
					Logger.LogWarning((object)("[BoatRace] RPC_RequestStart rejected -- " + failureReason));
					SendResult(sender, "Could not start race: " + failureReason);
					return;
				}
				BroadcastCountdown(Plugin.ConfigCountdownSeconds.Value, RaceManager.Instance.GateCount);
				BroadcastState();
				SendResult(sender, $"Race starting with {RaceManager.Instance.Racers.Count} racer(s).");
				WarnOnGateConfigMismatch(sender);
			}
		}

		private void RPC_RequestEnd(long sender, ZPackage pkg)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Logger.LogInfo((object)$"[BoatRace] RPC_RequestEnd from sender {sender}");
				if (!IsSenderAdmin(sender))
				{
					SendResult(sender, "You must be a server admin to end a race.");
					return;
				}
				if (!RaceManager.Instance.IsActive)
				{
					SendResult(sender, "No race is currently running.");
					return;
				}
				RaceManager.Instance.EndRace("ended early by admin");
				BroadcastRaceEnd("The boat race has been ended by an admin.");
				BroadcastState();
				SendResult(sender, "Race ended. Remaining racers marked DNF.");
			}
		}

		private void RPC_RequestReset(long sender, ZPackage pkg)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Logger.LogInfo((object)$"[BoatRace] RPC_RequestReset from sender {sender}");
				if (!IsSenderAdmin(sender))
				{
					SendResult(sender, "You must be a server admin to reset the race.");
					return;
				}
				RaceManager.Instance.ResetRace();
				BroadcastState();
				SendResult(sender, "Race state cleared.");
			}
		}

		private void RPC_RequestReload(long sender, ZPackage pkg)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Logger.LogInfo((object)$"[BoatRace] RPC_RequestReload from sender {sender}");
				if (!IsSenderAdmin(sender))
				{
					SendResult(sender, "You must be a server admin to reload the gate config.");
					return;
				}
				if (RaceManager.Instance.IsActive)
				{
					SendResult(sender, "Cannot reload gates while a race is in progress. End it first.");
					return;
				}
				bool flag = GateConfig.Load();
				SendResult(sender, flag ? $"Gate config reloaded on the server: {GateConfig.Gates.Count} gates. Each client reloads its own copy independently." : "Gate config reload FAILED on the server -- see the server log for the specific error. Previous gates kept.");
			}
		}

		public void SendRequestDebugState(string token)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(token);
				ZRoutedRpc.instance.InvokeRoutedRPC("BoatRace_RequestDebugState", new object[1] { val });
			}
		}

		private void RPC_RequestDebugState(long sender, ZPackage pkg)
		{
			string text = pkg.ReadString();
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Logger.LogInfo((object)$"[BoatRace] RPC_RequestDebugState from sender {sender} token={text}");
				if (!IsSenderAdmin(sender))
				{
					Logger.LogWarning((object)$"[BoatRace] RPC_RequestDebugState REJECTED -- sender {sender} is not admin. token={text}. NO DUMP WAS PRODUCED for this token: this is an admin-gate rejection, not an empty result.");
					SendResult(sender, "boatrace_debugstate: the server refused the dump -- you are not a server admin (token " + text + "). No server-side dump was produced.");
					return;
				}
				StateDump.Begin(text, "server-relayed");
				StateDump.DumpGateConfig(text);
				StateDump.DumpFingerprints(text);
				StateDump.DumpRaceState(text);
				StateDump.DumpPeers(text);
				StateDump.DumpPlayerCount(text, isServer: true);
				StateDump.End(text);
				SendResult(sender, "boatrace_debugstate: server dump written to the SERVER log under token " + text + ".");
			}
		}

		public void OnPeerConnected(long peerId, string playerName, string steamId)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				string opaqueKey = RaceManager.Instance.GetOpaqueKey(steamId);
				if (string.IsNullOrEmpty(opaqueKey))
				{
					Logger.LogWarning((object)$"[BoatRace] Peer {peerId} ('{playerName}') connected but no durable identity could be resolved -- they will not be able to see their own row.");
					return;
				}
				RaceManager.Instance.RebindPeer(opaqueKey, peerId, playerName);
				SendKeyTo(peerId, opaqueKey);
			}
		}

		private void SendKeyTo(long targetPeer, string key)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				if (Registered)
				{
					ZPackage val = new ZPackage();
					val.Write(key);
					ZRoutedRpc.instance.InvokeRoutedRPC(targetPeer, "BoatRace_AssignKey", new object[1] { val });
					Logger.LogInfo((object)$"[BoatRace] Assigned race identity {key} to peer {targetPeer}.");
				}
			}
		}

		private void RPC_AssignKey(long sender, ZPackage pkg)
		{
			if (!IsFromServer(sender))
			{
				Logger.LogWarning((object)$"[BoatRace] RPC_AssignKey rejected -- sender {sender} is not the server.");
				return;
			}
			string text = pkg.ReadString();
			if (!string.IsNullOrEmpty(text))
			{
				RaceManager.Instance.SetLocalKey(text);
			}
		}

		public void SendFingerprintToServer()
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Expected O, but got Unknown
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && ZRoutedRpc.instance != null)
			{
				Register();
				if (Registered)
				{
					string fingerprint = GateConfig.Fingerprint;
					ZPackage val = new ZPackage();
					val.Write(fingerprint);
					ZRoutedRpc.instance.InvokeRoutedRPC("BoatRace_ReportFingerprint", new object[1] { val });
					Logger.LogInfo((object)("[BoatRace] Reported gate config fingerprint " + fingerprint + " to the server."));
				}
			}
		}

		private void RPC_ReportFingerprint(long sender, ZPackage pkg)
		{
			string text = pkg.ReadString();
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				peerFingerprints[sender] = text;
				string fingerprint = GateConfig.Fingerprint;
				if (string.Equals(text, fingerprint, StringComparison.Ordinal))
				{
					Logger.LogInfo((object)$"[BoatRace] Peer {sender} reported gate config fingerprint {text} -- MATCHES the server ({fingerprint}).");
					return;
				}
				Logger.LogWarning((object)string.Format("[BoatRace] Peer {0} reported gate config fingerprint {1} but the server has {2} -- THIS MACHINE IS HOLDING A DIFFERENT COURSE. Its markers, pins and arrows will not match where the server detects gate crossings. Fix its {3} and have it run boatrace_reload.", sender, text, fingerprint, "BoatRace_Gates.json"));
			}
		}

		public void PruneFingerprints(HashSet<long> connectedPeerIds)
		{
			if (peerFingerprints.Count == 0 || connectedPeerIds == null)
			{
				return;
			}
			List<long> list = new List<long>();
			foreach (KeyValuePair<long, string> peerFingerprint in peerFingerprints)
			{
				if (!connectedPeerIds.Contains(peerFingerprint.Key))
				{
					list.Add(peerFingerprint.Key);
				}
			}
			foreach (long item in list)
			{
				peerFingerprints.Remove(item);
			}
		}

		public string GetPeerFingerprint(long peerId)
		{
			if (!peerFingerprints.TryGetValue(peerId, out var value))
			{
				return "<unknown>";
			}
			return value;
		}

		private void WarnOnGateConfigMismatch(long adminPeer)
		{
			string fingerprint = GateConfig.Fingerprint;
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			long uID = ZNet.GetUID();
			foreach (Racer value2 in RaceManager.Instance.Racers.Values)
			{
				if (value2.PeerId != uID)
				{
					if (!peerFingerprints.TryGetValue(value2.PeerId, out var value))
					{
						list2.Add(value2.PlayerName);
					}
					else if (!string.Equals(value, fingerprint, StringComparison.Ordinal))
					{
						list.Add(value2.PlayerName + " [" + value + "]");
					}
				}
			}
			if (list.Count == 0 && list2.Count == 0)
			{
				Logger.LogInfo((object)("[BoatRace] Gate config check passed: every racer reported the server's fingerprint " + fingerprint + "."));
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("GATE CONFIG MISMATCH -- the race HAS started, on the server's course (" + fingerprint + "), but not everyone is looking at it. ");
			if (list.Count > 0)
			{
				stringBuilder.Append("Different course: " + string.Join(", ", list.ToArray()) + ". ");
			}
			if (list2.Count > 0)
			{
				stringBuilder.Append("Never reported a course (old build, or not finished connecting): " + string.Join(", ", list2.ToArray()) + ". ");
			}
			stringBuilder.Append("Those players see buoys, pins and arrows in the wrong place, and their crossings are still judged against the SERVER's gates. Have them fix BoatRace_Gates.json and run boatrace_reload.");
			string text = stringBuilder.ToString();
			Logger.LogWarning((object)("[BoatRace] " + text));
			SendResult(adminPeer, text);
		}

		public void BroadcastRaceEnd(string message)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			if (ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(message);
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "BoatRace_AnnounceRaceEnd", new object[1] { val });
			}
		}

		private void OnStateChanged()
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				BroadcastState();
			}
		}

		private void OnRacerFinished(Racer racer)
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected O, but got Unknown
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(racer.PlayerName);
				val.Write(racer.FinishPlace);
				val.Write(racer.FinishTime);
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "BoatRace_AnnounceFinish", new object[1] { val });
			}
		}

		private void OnPenaltyApplied(Racer racer, int gateIndex)
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected O, but got Unknown
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZRoutedRpc.instance != null)
			{
				Register();
				ZPackage val = new ZPackage();
				val.Write(Plugin.ConfigPenaltySeconds.Value);
				val.Write(gateIndex);
				ZRoutedRpc.instance.InvokeRoutedRPC(racer.PeerId, "BoatRace_PenaltyNotice", new object[1] { val });
			}
		}
	}
}
namespace BoatRace.Markers
{
	public class GateMarkerManager
	{
		private class GateMarkers
		{
			public GameObject A;

			public GameObject B;

			public bool IsActive;

			public bool Spawned;
		}

		private const string PrefabActive = "piece_groundtorch_green";

		private const string PrefabInactive = "piece_groundtorch";

		private readonly Dictionary<int, GateMarkers> markers = new Dictionary<int, GateMarkers>();

		private int lastActiveGateIndex = -1;

		private bool visible;

		private ZNetScene spawnedInScene;

		public bool Visible => visible;

		public int ActiveGateIndex => lastActiveGateIndex;

		public int SpawnedMarkerPairs
		{
			get
			{
				int num = 0;
				foreach (GateMarkers value in markers.Values)
				{
					if (value.Spawned && (Object)(object)value.A != (Object)null && (Object)(object)value.B != (Object)null)
					{
						num++;
					}
				}
				return num;
			}
		}

		public GateMarkerManager()
		{
			GateConfig.OnGatesReloaded += Rebuild;
		}

		public void SetVisible(bool value)
		{
			if (visible != value)
			{
				visible = value;
				if (!visible)
				{
					DestroyAll();
				}
				else
				{
					Rebuild();
				}
			}
		}

		public void Rebuild()
		{
			DestroyAll();
			if (!visible)
			{
				return;
			}
			if ((Object)(object)ZNetScene.instance == (Object)null)
			{
				Logger.LogWarning((object)"[BoatRace] Cannot spawn gate markers -- ZNetScene.instance is null (world not loaded yet).");
				return;
			}
			lastActiveGateIndex = -1;
			foreach (Gate gate in GateConfig.Gates)
			{
				markers[gate.Index] = new GateMarkers();
			}
			spawnedInScene = ZNetScene.instance;
			RefreshActiveGate(force: true);
		}

		public void UpdateFrame(int activeGateIndex)
		{
			if (!visible)
			{
				return;
			}
			if (spawnedInScene != ZNetScene.instance)
			{
				if ((Object)(object)ZNetScene.instance == (Object)null)
				{
					markers.Clear();
					spawnedInScene = null;
					lastActiveGateIndex = -1;
				}
				else
				{
					Logger.LogInfo((object)"[BoatRace] World reloaded -- respawning gate markers.");
					Rebuild();
					lastActiveGateIndex = activeGateIndex;
					RefreshActiveGate(force: true);
				}
			}
			else if (activeGateIndex != lastActiveGateIndex)
			{
				lastActiveGateIndex = activeGateIndex;
				RefreshActiveGate(force: false);
			}
		}

		public void NotifyWorldExit()
		{
			markers.Clear();
			spawnedInScene = null;
			lastActiveGateIndex = -1;
			visible = false;
		}

		private void RefreshActiveGate(bool force)
		{
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)ZNetScene.instance == (Object)null)
			{
				return;
			}
			foreach (Gate gate in GateConfig.Gates)
			{
				if (!markers.TryGetValue(gate.Index, out var value))
				{
					value = new GateMarkers();
					markers[gate.Index] = value;
				}
				bool flag = gate.Index == lastActiveGateIndex;
				if (!value.Spawned || value.IsActive != flag || force)
				{
					DestroyMarkers(value);
					string prefabName = (flag ? "piece_groundtorch_green" : "piece_groundtorch");
					value.A = SpawnLocal(prefabName, gate.PointA);
					value.B = SpawnLocal(prefabName, gate.PointB);
					value.IsActive = flag;
					value.Spawned = true;
					Color color = (flag ? Plugin.ActiveMarkerColor : Plugin.InactiveMarkerColor);
					ApplyTint(value.A, color);
					ApplyTint(value.B, color);
				}
			}
		}

		private static GameObject SpawnLocal(string prefabName, Vector3 position)
		{
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0154: 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)
			GameObject prefab = ZNetScene.instance.GetPrefab(prefabName);
			if ((Object)(object)prefab == (Object)null)
			{
				Logger.LogError((object)("[BoatRace] Gate marker prefab '" + prefabName + "' not found in ZNetScene."));
				return null;
			}
			ZNetView.m_forceDisableInit = true;
			TerrainOp.m_forceDisableTerrainOps = true;
			GameObject val;
			try
			{
				val = Object.Instantiate<GameObject>(prefab, position, Quaternion.identity);
			}
			finally
			{
				ZNetView.m_forceDisableInit = false;
				TerrainOp.m_forceDisableTerrainOps = false;
			}
			if ((Object)(object)val == (Object)null)
			{
				return null;
			}
			((Object)val).name = "BoatRace_GateMarker";
			Joint[] componentsInChildren = val.GetComponentsInChildren<Joint>();
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren[i]);
			}
			Rigidbody[] componentsInChildren2 = val.GetComponentsInChildren<Rigidbody>();
			for (int i = 0; i < componentsInChildren2.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren2[i]);
			}
			Collider[] componentsInChildren3 = val.GetComponentsInChildren<Collider>();
			for (int i = 0; i < componentsInChildren3.Length; i++)
			{
				componentsInChildren3[i].enabled = false;
			}
			TerrainModifier[] componentsInChildren4 = val.GetComponentsInChildren<TerrainModifier>();
			for (int i = 0; i < componentsInChildren4.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren4[i]);
			}
			Piece[] componentsInChildren5 = val.GetComponentsInChildren<Piece>();
			for (int i = 0; i < componentsInChildren5.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren5[i]);
			}
			WearNTear[] componentsInChildren6 = val.GetComponentsInChildren<WearNTear>();
			for (int i = 0; i < componentsInChildren6.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren6[i]);
			}
			Fireplace[] componentsInChildren7 = val.GetComponentsInChildren<Fireplace>();
			for (int i = 0; i < componentsInChildren7.Length; i++)
			{
				Object.Destroy((Object)(object)componentsInChildren7[i]);
			}
			val.transform.localScale = prefab.transform.localScale * Plugin.ConfigMarkerScale.Value;
			return val;
		}

		private static void ApplyTint(GameObject go, Color color)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)go == (Object)null) && !((Object)(object)MaterialMan.instance == (Object)null))
			{
				MaterialMan.instance.SetValue<Color>(go, ShaderProps._Color, color, false);
				MaterialMan.instance.SetValue<Color>(go, ShaderProps._EmissionColor, color * Plugin.ConfigMarkerEmission.Value, false);
			}
		}

		private static void DestroyMarkers(GateMarkers gm)
		{
			if ((Object)(object)gm.A != (Object)null)
			{
				Object.Destroy((Object)(object)gm.A);
			}
			if ((Object)(object)gm.B != (Object)null)
			{
				Object.Destroy((Object)(object)gm.B);
			}
			gm.A = null;
			gm.B = null;
			gm.Spawned = false;
		}

		public void Dispose()
		{
			GateConfig.OnGatesReloaded -= Rebuild;
			DestroyAll();
		}

		public void DestroyAll()
		{
			foreach (GateMarkers value in markers.Values)
			{
				DestroyMarkers(value);
			}
			markers.Clear();
			lastActiveGateIndex = -1;
			spawnedInScene = null;
		}
	}
	public class MapPinManager
	{
		private const PinType PinTypeNextGate = (PinType)3;

		private PinData pin;

		private int pinnedGateIndex = -1;

		private bool enabled;

		public bool HasLivePin => pin != null;

		public int PinnedGateIndex => pinnedGateIndex;

		public bool IsEnabled => enabled;

		public void SetEnabled(bool value)
		{
			if (enabled != value)
			{
				enabled = value;
				if (!enabled)
				{
					RemovePin();
				}
			}
		}

		public void UpdateFrame(int nextGateIndex)
		{
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			if (!enabled)
			{
				RemovePin();
			}
			else if (!((Object)(object)Minimap.instance == (Object)null))
			{
				Gate gate = GateConfig.GetGate(nextGateIndex);
				if (gate == null)
				{
					RemovePin();
				}
				else if (pin == null || pinnedGateIndex != nextGateIndex)
				{
					CreatePin(gate, nextGateIndex);
				}
				else if (pin.m_pos != gate.Center)
				{
					CreatePin(gate, nextGateIndex);
				}
			}
		}

		private void CreatePin(Gate gate, int gateIndex)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			RemovePin();
			pin = Minimap.instance.AddPin(gate.Center, (PinType)3, gate.Name, false, false, 0L, default(PlatformUserID));
			if (pin != null)
			{
				pin.m_animate = true;
				pin.m_doubleSize = true;
			}
			pinnedGateIndex = gateIndex;
		}

		public void RemovePin()
		{
			if (pin != null)
			{
				if ((Object)(object)Minimap.instance != (Object)null)
				{
					Minimap.instance.RemovePin(pin);
				}
				pin = null;
				pinnedGateIndex = -1;
			}
		}
	}
}
namespace BoatRace.Diagnostics
{
	public static class StateDump
	{
		public static string NewToken()
		{
			return Guid.NewGuid().ToString("N").Substring(0, 8);
		}

		public static void Begin(string token, string origin)
		{
			Logger.LogInfo((object)("[BoatRace][DIAG] ===== debugstate BEGIN token=" + token + " origin=" + origin + " ====="));
			Logger.LogInfo((object)$"[BoatRace][DIAG] token={token} header znetNonNull={(Object)(object)ZNet.instance != (Object)null} isServer={(Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()} myUid={(((Object)(object)ZNet.instance != (Object)null) ?