Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of BoatRace v0.3.0
BoatRace.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.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) ?