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 ScoreRace v4.1.0
ScoreRace.dll
Decompiled 4 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Photon.Pun; using Photon.Realtime; using Photon.Voice; using Photon.Voice.Unity; using REPOJP.ScoreRace.Core; using Steamworks; using Steamworks.Data; using TMPro; using UnityEngine; using UnityEngine.AI; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("REPOJP")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("4.1.0.0")] [assembly: AssemblyInformationalVersion("4.1.0")] [assembly: AssemblyProduct("ScoreRace")] [assembly: AssemblyTitle("ScoreRace")] [assembly: AssemblyVersion("4.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [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 REPOJP.ScoreRace { internal sealed class CosmeticTracker { internal sealed class Pending { public int Before; public int Rarity; public CosmeticDelivery Delivery; } private readonly Dictionary<int, CosmeticBox> boxes = new Dictionary<int, CosmeticBox>(); private readonly Dictionary<string, CosmeticDelivery> delivered = new Dictionary<string, CosmeticDelivery>(); public CosmeticBox[] Initial => boxes.Values.OrderBy<CosmeticBox, string>((CosmeticBox b) => b.Id, StringComparer.Ordinal).ToArray(); public CosmeticDelivery[] Delivered => delivered.Values.OrderBy<CosmeticDelivery, string>((CosmeticDelivery b) => b.Id, StringComparer.Ordinal).ToArray(); public int Count => delivered.Count; public int ReferenceWeight => Cosmetics.ReferenceWeight(Initial); public double Bonus => Cosmetics.Bonus(Delivered); private static List<Rarity> Extracted => Access.Read<List<Rarity>>(RoundDirector.instance, "cosmeticWorldObjectsExtracted"); public void Restore(CosmeticDelivery[] values) { if (!Cosmetics.Matches(values, Initial)) { throw new InvalidOperationException("Invalid team cosmetic checkpoint."); } delivered.Clear(); foreach (CosmeticDelivery cosmeticDelivery in values) { delivered[cosmeticDelivery.Id] = cosmeticDelivery; } } public void Clear() { boxes.Clear(); delivered.Clear(); } public IEnumerable<string> Capture() { Clear(); CosmeticWorldObject[] array = Object.FindObjectsOfType<CosmeticWorldObject>(); foreach (CosmeticWorldObject val in array) { NotValuableObject component = ((Component)val).GetComponent<NotValuableObject>(); if ((Object)(object)component == (Object)null || !component.hasHealth || component.healthMax <= 0) { throw new InvalidOperationException("A cosmetic box has no valid health data."); } Cosmetics.RarityWeight((int)val.rarity); int num = Access.Read<int>(component, "healthCurrent"); if (num < 0 || num > component.healthMax) { throw new InvalidOperationException("Invalid initial cosmetic box health."); } string text = GameBridge.Pose(((Component)val).transform); string id = Hash.Text("cosmetic:" + text); if (boxes.Values.Any((CosmeticBox b) => b.Id == id)) { throw new InvalidOperationException("Cosmetic box positions are ambiguous."); } if (boxes.Count >= 1024) { throw new InvalidOperationException("Too many cosmetic boxes."); } boxes.Add(TeamRuntime.ObjectId((Component)(object)val), new CosmeticBox { Id = id, Rarity = (int)val.rarity, MaximumHealth = component.healthMax, InitialHealth = num }); yield return "cosmetic:" + text + ":" + ((int)val.rarity).ToString(CultureInfo.InvariantCulture) + ":" + num.ToString(CultureInfo.InvariantCulture) + "/" + component.healthMax.ToString(CultureInfo.InvariantCulture); } } public Pending? BeforeExtract(CosmeticWorldObject box) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected I4, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected I4, but got Unknown if (!boxes.TryGetValue(TeamRuntime.ObjectId((Component)(object)box), out CosmeticBox value) || delivered.ContainsKey(value.Id)) { return null; } NotValuableObject component = ((Component)box).GetComponent<NotValuableObject>(); return new Pending { Before = Extracted.Count, Rarity = (int)box.rarity, Delivery = new CosmeticDelivery { Id = value.Id, Rarity = (int)box.rarity, Health = Math.Max(0, Access.Read<int>(component, "healthCurrent")), MaximumHealth = component.healthMax } }; } public void AfterExtract(Pending? pending) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 if (pending != null && Extracted.Count == pending.Before + 1 && (int)Extracted[Extracted.Count - 1] == pending.Rarity) { if (!Cosmetics.Matches(new CosmeticDelivery[1] { pending.Delivery }, Initial)) { throw new InvalidOperationException("Invalid cosmetic box extraction health."); } if (!delivered.ContainsKey(pending.Delivery.Id)) { delivered.Add(pending.Delivery.Id, pending.Delivery); } } } } internal sealed class DeadVoice : IDisposable { private sealed class Playback : IDisposable { private readonly Queue<float> queue = new Queue<float>(); private readonly object gate = new object(); public readonly AudioSource Source; public int Sequence = -1; public double At; private readonly AudioClip clip; public Playback(string id) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown GameObject val = new GameObject("ScoreRace Dead Voice " + id); Object.DontDestroyOnLoad((Object)(object)val); Source = val.AddComponent<AudioSource>(); Source.spatialBlend = 0f; Source.loop = true; Source.volume = 0.5f; clip = AudioClip.Create("ScoreRace dead voice", 8000, 1, 8000, true, new PCMReaderCallback(Read)); Source.clip = clip; Source.Play(); } private void Read(float[] data) { lock (gate) { for (int i = 0; i < data.Length; i++) { data[i] = ((queue.Count > 0) ? queue.Dequeue() : 0f); } } } public void Add(byte[] data) { lock (gate) { if (queue.Count > 1600) { queue.Clear(); } for (int i = 25; i < data.Length; i++) { queue.Enqueue(MediaPackets.MuLaw(data[i])); } } } public void Dispose() { Source.Stop(); Object.Destroy((Object)(object)((Component)Source).gameObject); Object.Destroy((Object)(object)clip); lock (gate) { queue.Clear(); } } } private const int Channel = 95; private const int Rate = 8000; private const int Samples = 160; private readonly Plugin p; private readonly object captureLock = new object(); private readonly Queue<byte[]> outgoing = new Queue<byte[]>(); private readonly List<byte> frame = new List<byte>(160); private readonly Dictionary<string, Playback> speakers = new Dictionary<string, Playback>(); private readonly Dictionary<AudioSource, bool> muted = new Dictionary<AudioSource, bool>(); private volatile object? sourceVoice; private volatile bool blockNative; private volatile bool capture; private volatile int sourceRate = 24000; private volatile int sourceChannels = 1; private volatile float gain = 1f; private double resample; private int sequence; private volatile int captureEpoch; private volatile int captureChannel; private DeadVoiceChannel selectedChannel = DeadVoiceChannel.Global; private string channelRound = ""; private RectTransform? channelRoot; private SimpleButton? channelButton; private string round = ""; private string policy = ""; private string micDevice = ""; private byte[] roundBytes = Array.Empty<byte>(); private AudioClip? mic; private int micPosition; public DeadVoiceChannel EffectiveChannel => VoiceAudience.Effective(p.Race.Observing ? (-1) : (p.Race.Plan?.Team(p.Race.Net.Self)?.Number).GetValueOrDefault(), p.Race.LocalEliminated, (!(channelRound == p.Race.Plan?.Id)) ? DeadVoiceChannel.Global : selectedChannel); private bool CanSwitch { get { if (p.Game.Active && p.Race.Multiplayer && p.Race.Plan != null && !p.Race.Observing && p.Race.Plan.Team(p.Race.Net.Self) != null && p.Race.LocalDead && p.Race.LocalEliminated && p.Race.State >= Phase.Countdown) { return p.Race.State <= Phase.Finished; } return false; } } public DeadVoice(Plugin plugin) { p = plugin; } private void ClearAudio() { capture = false; lock (captureLock) { captureEpoch++; outgoing.Clear(); frame.Clear(); resample = 0.0; } foreach (Playback value in speakers.Values) { value.Dispose(); } speakers.Clear(); } private void SwitchChannel() { if (CanSwitch) { ClearAudio(); selectedChannel = ((EffectiveChannel != DeadVoiceChannel.Global) ? DeadVoiceChannel.Global : DeadVoiceChannel.Team); channelRound = p.Race.Plan.Id; policy = ""; } } public void Render() { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) if (!CanSwitch || ResultButtons.NativeMenuOpen || ((Object)(object)LoadingUI.instance != (Object)null && ((Component)LoadingUI.instance).gameObject.activeInHierarchy)) { if ((Object)(object)channelRoot != (Object)null) { ((Component)channelRoot).gameObject.SetActive(false); } return; } if ((Object)(object)channelRoot == (Object)null) { BigMessageUI instance = BigMessageUI.instance; TextMeshProUGUI val = ((instance != null) ? ((Component)instance).GetComponent<TextMeshProUGUI>() : null); if ((Object)(object)val == (Object)null) { return; } channelRoot = new GameObject("ScoreRace Voice Channel", new Type[2] { typeof(RectTransform), typeof(Canvas) }).GetComponent<RectTransform>(); Object.DontDestroyOnLoad((Object)(object)((Component)channelRoot).gameObject); Canvas component = ((Component)channelRoot).GetComponent<Canvas>(); component.renderMode = (RenderMode)0; component.sortingOrder = 29000; channelButton = new SimpleButton(channelRoot, val, ""); } ((Component)channelRoot).gameObject.SetActive(true); float scale = Mathf.Min((float)Screen.width / 1280f, (float)Screen.height / 720f); channelButton.Place(423f, 343f, 400f, 30f, 22f, scale); ((TMP_Text)channelButton.Label).text = "VOICE: " + ((EffectiveChannel == DeadVoiceChannel.Team) ? "TEAM" : "GLOBAL") + " [F2]"; if (channelButton.Draw()) { SwitchChannel(); } } public unsafe void Tick() { //IL_04c2: Unknown result type (might be due to invalid IL or missing references) //IL_04c7: Unknown result type (might be due to invalid IL or missing references) //IL_04df: Unknown result type (might be due to invalid IL or missing references) //IL_046a: Unknown result type (might be due to invalid IL or missing references) string text = p.Race.Plan?.Id ?? ""; if (channelRound != text) { channelRound = text; selectedChannel = DeadVoiceChannel.Global; } if (CanSwitch && Application.isFocused && !ResultButtons.NativeMenuOpen && Input.GetKeyDown((KeyCode)283)) { SwitchChannel(); } bool flag = p.Game.Active && p.Race.Multiplayer && p.Race.Plan != null && p.Race.State >= Phase.Countdown && p.Race.State <= Phase.Finished; bool dead = flag && p.Race.LocalDead; blockNative = dead; PlayerVoiceChat instance = PlayerVoiceChat.instance; Recorder val = (((Object)(object)instance != (Object)null) ? Access.Read<Recorder>(instance, "recorder") : null); sourceVoice = (((Object)(object)val != (Object)null) ? Access.Read<LocalVoice>(val, "voice") : null); if ((Object)(object)val != (Object)null) { IAudioDesc val2 = Access.Read<IAudioDesc>(val, "inputSource"); if (val2 != null) { sourceRate = val2.SamplingRate; sourceChannels = Math.Max(1, val2.Channels); } } gain = (((Object)(object)instance != (Object)null) ? Mathf.Clamp(Access.Read<float>(instance, "microphoneVolumeMultiplier"), 0f, 4f) : 1f); string[] array = (flag ? p.Race.Participants.Where((string id) => id != p.Race.Net.Self && p.Spectators.CanTalk(id)).ToArray() : Array.Empty<string>()); string text2 = (flag ? p.Race.Plan.Id : ""); string text3 = text2 + ":" + dead + ":" + p.Race.LocalEliminated + ":" + EffectiveChannel.ToString() + ":" + string.Join("|", array); if (policy != text3) { ClearAudio(); policy = text3; } if (round != text2) { round = text2; roundBytes = ((round.Length == 32) ? new Guid(round).ToByteArray() : Array.Empty<byte>()); sequence = 0; } captureChannel = (int)EffectiveChannel; bool flag2 = (Object)(object)instance == (Object)null || Access.Read<bool>(instance, "microphoneEnabled"); capture = dead && array.Length != 0 && flag2 && (Object)(object)DataDirector.instance != (Object)null && !Access.Read<bool>(DataDirector.instance, "toggleMute") && ((Object)(object)AudioManager.instance == (Object)null || !Access.Read<bool>(AudioManager.instance, "pushToTalk") || SemiFunc.InputHold((InputKey)25)); if (dead && sourceVoice == null) { FallbackMicrophone(); } else { StopMicrophone(); } if (flag) { PlayerVoiceChat[] array2 = Object.FindObjectsOfType<PlayerVoiceChat>(); foreach (PlayerVoiceChat voice in array2) { GateNative(voice); } } else { RestoreMute(); } if (!SteamClient.IsValid) { return; } for (int num2 = 0; num2 < 8; num2++) { byte[] array3; lock (captureLock) { array3 = ((outgoing.Count > 0) ? outgoing.Dequeue() : null); } if (array3 == null) { break; } if (!capture) { continue; } foreach (string item in array.Where(p.Spectators.CanTalk)) { SteamNetworking.SendP2PPacket(SteamId.op_Implicit(ulong.Parse(item)), array3, array3.Length, 95, (P2PSend)0); } } for (int num3 = 0; num3 < 256; num3++) { if (!SteamNetworking.IsP2PPacketAvailable(95)) { break; } P2Packet? val3 = SteamNetworking.ReadP2PPacket(95); if (!val3.HasValue) { break; } P2Packet value = val3.Value; string text4 = ((object)(*(SteamId*)(&value.SteamId))/*cast due to .constrained prefix*/).ToString(); byte[] data = val3.Value.Data; if (!dead || !p.Spectators.CanTalk(text4) || data.Length != 185) { continue; } using BinaryReader binaryReader = new BinaryReader(new MemoryStream(data)); if (binaryReader.ReadInt32() != 1397899576 || new Guid(binaryReader.ReadBytes(16)).ToString("N") != round) { continue; } int num4 = binaryReader.ReadInt32(); DeadVoiceChannel deadVoiceChannel = (DeadVoiceChannel)binaryReader.ReadByte(); if (num4 < 0 || deadVoiceChannel != EffectiveChannel || deadVoiceChannel != p.Spectators.Presence(text4)?.VoiceChannel) { continue; } if (!speakers.TryGetValue(text4, out Playback value2)) { value2 = (speakers[text4] = new Playback(text4)); } if (num4 > value2.Sequence) { value2.Sequence = num4; value2.At = Transport.Now; value2.Add(data); if ((Object)(object)instance != (Object)null) { value2.Source.outputAudioMixerGroup = instance.mixerMicrophoneSpectate; } } } string[] array4 = speakers.Keys.Where((string id) => !dead || !p.Spectators.CanTalk(id) || Transport.Now - speakers[id].At > 3.0).ToArray(); foreach (string key in array4) { speakers[key].Dispose(); speakers.Remove(key); } } public bool Input(object voice, float[] samples) { if (voice != sourceVoice || !blockNative) { return true; } int num = captureEpoch; if (capture) { lock (captureLock) { if (num == captureEpoch) { Feed(samples, sourceRate, sourceChannels); } } } return false; } public bool Input(object voice, short[] samples) { if (voice != sourceVoice || !blockNative) { return true; } if (capture) { int num = captureEpoch; float[] array = new float[samples.Length]; for (int i = 0; i < samples.Length; i++) { array[i] = (float)samples[i] / 32768f; } lock (captureLock) { if (num == captureEpoch) { Feed(array, sourceRate, sourceChannels); } } } return false; } private void Feed(float[] samples, int rate, int channels) { if (!capture || roundBytes.Length != 16 || rate < 8000 || rate > 192000 || channels < 1 || channels > 8) { return; } for (int i = 0; i + channels <= samples.Length; i += channels) { resample += 8000.0; if (resample < (double)rate) { continue; } resample -= rate; float num = 0f; for (int j = 0; j < channels; j++) { num += samples[i + j]; } frame.Add(MediaPackets.MuLaw(num / (float)channels * gain)); if (frame.Count != 160) { continue; } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(1397899576); binaryWriter.Write(roundBytes); binaryWriter.Write(sequence++); binaryWriter.Write((byte)captureChannel); binaryWriter.Write(frame.ToArray()); frame.Clear(); if (outgoing.Count >= 10) { outgoing.Dequeue(); } outgoing.Enqueue(memoryStream.ToArray()); } } private void FallbackMicrophone() { string text = (((Object)(object)SessionManager.instance != (Object)null) ? Access.Read<string>(SessionManager.instance, "micDeviceCurrent") : ""); if (!capture) { StopMicrophone(); return; } if ((Object)(object)mic == (Object)null || text != micDevice) { StopMicrophone(); if (!string.IsNullOrEmpty(text) && Microphone.devices.Contains(text)) { micDevice = text; mic = Microphone.Start(string.IsNullOrEmpty(text) ? null : text, true, 1, 8000); micPosition = 0; } return; } int position = Microphone.GetPosition(string.IsNullOrEmpty(micDevice) ? null : micDevice); if (position < 0) { return; } int num = (position - micPosition + mic.samples) % mic.samples; while (num >= 160) { float[] array = new float[160 * mic.channels]; mic.GetData(array, micPosition); micPosition = (micPosition + 160) % mic.samples; num -= 160; lock (captureLock) { Feed(array, mic.frequency, mic.channels); } } } private void StopMicrophone() { if ((Object)(object)mic != (Object)null) { Microphone.End(string.IsNullOrEmpty(micDevice) ? null : micDevice); Object.Destroy((Object)(object)mic); mic = null; micPosition = 0; } } public void GateNative(PlayerVoiceChat voice) { if (!p.Game.Active || !p.Race.Multiplayer) { return; } PlayerAvatar val = Access.Read<PlayerAvatar>(voice, "playerAvatar"); if ((Object)(object)val == (Object)null) { return; } bool localDead = p.Race.LocalDead; bool flag = Access.Read<bool>(val, "deadSet"); if ((Object)(object)val == (Object)(object)PlayerAvatar.instance && localDead) { Recorder val2 = Access.Read<Recorder>(voice, "recorder"); if ((Object)(object)val2 != (Object)null) { val2.TransmitEnabled = false; } Access.Set(voice, "isTalking", false); } bool flag2 = localDead || flag; AudioSource[] array = (AudioSource[])(object)new AudioSource[2] { Access.Read<AudioSource>(voice, "audioSource"), voice.ttsAudioSource }; foreach (AudioSource val3 in array) { if ((Object)(object)val3 == (Object)null) { continue; } bool value; if (flag2) { if (!muted.ContainsKey(val3)) { muted[val3] = val3.mute; } val3.mute = true; } else if (muted.TryGetValue(val3, out value)) { val3.mute = value; muted.Remove(val3); } } } private void RestoreMute() { foreach (KeyValuePair<AudioSource, bool> item in muted) { if ((Object)(object)item.Key != (Object)null) { item.Key.mute = item.Value; } } muted.Clear(); } public void Dispose() { if ((Object)(object)channelRoot != (Object)null) { Object.Destroy((Object)(object)((Component)channelRoot).gameObject); } channelRoot = null; capture = (blockNative = false); sourceVoice = null; StopMicrophone(); RestoreMute(); foreach (Playback value in speakers.Values) { value.Dispose(); } speakers.Clear(); lock (captureLock) { frame.Clear(); outgoing.Clear(); } } } internal sealed class DeliveryTracker { internal sealed class AbsorbStart { public object[] Before = Array.Empty<object>(); public int Source; } internal sealed class BoxUpdate { public object[] Before = Array.Empty<object>(); public object[] Ready = Array.Empty<object>(); public float Value; } public readonly DeliveryLedger Ledger = new DeliveryLedger(); public int ExtractionDepth; public int TaxReturnDepth; private readonly Dictionary<object, int> absorbedSources = new Dictionary<object, int>(); public void Clear() { Ledger.Clear(); absorbedSources.Clear(); ExtractionDepth = (TaxReturnDepth = 0); } public void Capture(IEnumerable<ValuableObject> valuables) { Clear(); foreach (ValuableObject valuable in valuables) { Ledger.Register(TeamRuntime.ObjectId((Component)(object)valuable), (long)Access.Read<float>(valuable, "dollarValueOriginal")); } } public int Source(ValuableObject v) { int num = TeamRuntime.ObjectId((Component)(object)v); if (!Ledger.Known(num)) { if (!Access.Read<bool>(v, "dollarValueSet")) { throw new InvalidOperationException("Delivered valuable price is not ready."); } Ledger.Register(num, (long)Access.Read<float>(v, "dollarValueOriginal")); } return num; } private static object[] Absorptions(ItemValuableBox box) { return Access.Read<IList>(box, "activeAbsorptions").Cast<object>().ToArray(); } public AbsorbStart BeforeAbsorb(ItemValuableBox box, PhysGrabObject target) { ValuableObject component = ((Component)target).GetComponent<ValuableObject>(); return new AbsorbStart { Before = Absorptions(box), Source = (((Object)(object)component != (Object)null) ? Source(component) : 0) }; } public void AfterAbsorb(ItemValuableBox box, AbsorbStart state) { foreach (object item in Absorptions(box).Except(state.Before)) { absorbedSources[item] = state.Source; } } public BoxUpdate BeforeUpdate(ItemValuableBox box) { object[] array = Absorptions(box); return new BoxUpdate { Before = array, Value = box.CurrentValue, Ready = (from entry in array.Reverse() where (Object)(object)Access.Read<GameObject>(entry, "meshCopy") != (Object)null && Mathf.Clamp01(Access.Read<float>(entry, "timer") / (Access.Read<float>(entry, "effectiveDuration") * 2.2f)) >= 1f select entry).ToArray() }; } public void AfterUpdate(ItemValuableBox box, BoxUpdate state) { object[] second = Absorptions(box); object[] array = state.Ready.Except(second).ToArray(); float num = state.Value; object[] array2 = array; foreach (object o in array2) { num = Mathf.Min(num + Access.Read<float>(o, "valueToAdd"), box.valueLimit); } if (Math.Abs(num - box.CurrentValue) > 0.05f) { throw new InvalidOperationException("Valuable box accounting changed during absorption."); } float num2 = state.Value; array2 = array; foreach (object obj in array2) { float num3 = Mathf.Min(num2 + Access.Read<float>(obj, "valueToAdd"), box.valueLimit); if (absorbedSources.TryGetValue(obj, out var value)) { Ledger.Store(TeamRuntime.ObjectId((Component)(object)box), value, (long)Math.Max(0f, num3 - num2)); } num2 = num3; } foreach (object item in state.Before.Except(second)) { absorbedSources.Remove(item); } } } internal static class Determinism { private sealed class Stream : IEnumerator, IDisposable { private readonly IEnumerator inner; private State state; private readonly bool pause; private bool held; public object? Current { get { if (!held) { return inner.Current; } return null; } } public Stream(IEnumerator inner, int seed, bool pause) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) this.inner = inner; this.pause = pause; State val = Random.state; Random.InitState(seed); state = Random.state; Random.state = val; } public bool MoveNext() { //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) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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) held = pause && Plugin.Instance.Game.Active && Plugin.Instance.Game.Frozen; if (held) { return true; } if (!Plugin.Instance.Game.Active) { return inner.MoveNext(); } State val = Random.state; Random.state = state; try { return inner.MoveNext(); } finally { state = Random.state; Random.state = val; } } public void Reset() { throw new NotSupportedException(); } public void Dispose() { if (inner is IDisposable disposable) { disposable.Dispose(); } } } private static readonly Dictionary<string, int> calls = new Dictionary<string, int>(); public static bool Enabled { get { Plugin instance = Plugin.Instance; if (instance != null && instance.Game.Active) { Race race = Plugin.Instance.Race; if (race == null) { return false; } return race.State == Phase.Generating; } return false; } } public static void Reset() { calls.Clear(); } private static int Seed(object? instance, MethodBase method) { string text = method.DeclaringType.Name + "." + method.Name; Component val = (Component)((instance is Component) ? instance : null); if (val != null) { text = text + "/" + GameBridge.Pose(val.transform); } calls.TryGetValue(text, out var value); calls[text] = value + 1; return Hash.Seed(Plugin.Instance.Game.Plan.Seed + "/" + text + "/" + value); } public static void Enter(object? __instance, MethodBase __originalMethod, out State? __state) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) __state = null; if (Enabled) { __state = Random.state; Random.InitState(Seed(__instance, __originalMethod)); } } public static Exception? Leave(Exception? __exception, State? __state) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) if (__state.HasValue) { Random.state = __state.Value; } return __exception; } public static void Wrap(object? __instance, MethodBase __originalMethod, ref IEnumerator __result) { if (Enabled && __result != null) { __result = new Stream(__result, Seed(__instance, __originalMethod), __originalMethod.DeclaringType == typeof(EnemyParent) && (__originalMethod.Name == "Logic" || __originalMethod.Name == "PlayerCloseLogic")); } } public static void Install(Harmony h) { //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Expected O, but got Unknown //IL_0206: Expected O, but got Unknown string[] array = new string[17] { "LevelGenerator", "ValuableDirector", "ValuableObject", "Module", "ModulePropSwitch", "ModulePropLight", "ModuleProp", "EnemyDirector", "EnemyParent", "Enemy", "RoundDirector", "ExtractionPoint", "LevelPoint", "LevelPointEnemy", "LevelPointNavMesh", "PhysGrabObject", "ItemAttributes" }; foreach (string name in array) { Type type = typeof(LevelGenerator).Assembly.GetType(name, throwOnError: false); if (type == null) { continue; } MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!methodInfo.IsGenericMethod && !methodInfo.IsAbstract) { if (methodInfo.ReturnType == typeof(IEnumerator)) { h.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Determinism), "Wrap", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else if (methodInfo.Name == "Start" || methodInfo.Name == "Awake" || methodInfo.Name.Contains("Setup") || methodInfo.Name.Contains("Generation") || methodInfo.Name.Contains("Spawn") || methodInfo.Name == "DollarValueSetLogic" || methodInfo.Name == "GetEnemy" || methodInfo.Name == "GetValue") { h.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Determinism), "Enter", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Determinism), "Leave", (Type[])null), (HarmonyMethod)null); } } } } } } internal sealed class EnvironmentScan { private readonly Dictionary<string, (long length, long ticks, string hash)> cache = new Dictionary<string, (long, long, string)>(StringComparer.OrdinalIgnoreCase); private readonly string ownConfig; public EnvironmentScan(string ownConfigPath) { ownConfig = Path.GetFullPath(ownConfigPath); } public Task<Manifest> Capture(bool force) { List<ManifestEntry> entries = new List<ManifestEntry>(); List<string> errors = new List<string>(); Dictionary<string, SortedDictionary<string, string>> configs = new Dictionary<string, SortedDictionary<string, string>>(StringComparer.OrdinalIgnoreCase); Add("runtime", "game", Application.version, Application.version); Add("runtime", "loader", typeof(BaseUnityPlugin).Assembly.GetName().Version.ToString(), typeof(BaseUnityPlugin).Assembly.GetName().Version.ToString()); foreach (PluginInfo item in Chainloader.PluginInfos.Values.OrderBy<PluginInfo, string>((PluginInfo x) => x.Metadata.GUID, StringComparer.Ordinal)) { Add("mod", item.Metadata.GUID, item.Metadata.Version.ToString(), item.Metadata.Name + " " + item.Metadata.Version); if ((Object)(object)item.Instance == (Object)null) { errors.Add("Cannot inspect plugin: " + item.Metadata.GUID); continue; } string fullPath = Path.GetFullPath(item.Instance.Config.ConfigFilePath); if (Same(fullPath, ownConfig)) { continue; } if (!configs.TryGetValue(fullPath, out SortedDictionary<string, string> value)) { value = (configs[fullPath] = new SortedDictionary<string, string>(StringComparer.Ordinal)); } foreach (KeyValuePair<ConfigDefinition, ConfigEntryBase> item2 in item.Instance.Config) { value[item2.Key.Section + "\0" + item2.Key.Key] = item2.Value.GetSerializedValue(); } } return Task.Run(delegate { try { HashSet<string> hashSet = new HashSet<string>(configs.Keys, StringComparer.OrdinalIgnoreCase); if (Directory.Exists(Paths.ConfigPath)) { foreach (string item3 in Directory.EnumerateFiles(Paths.ConfigPath, "*", SearchOption.AllDirectories)) { hashSet.Add(Path.GetFullPath(item3)); } } foreach (string item4 in hashSet.OrderBy<string, string>((string x) => x, StringComparer.OrdinalIgnoreCase)) { if (!Same(item4, ownConfig)) { string text = Logical(item4, Paths.ConfigPath); try { SortedDictionary<string, string> sortedDictionary2 = new SortedDictionary<string, string>(StringComparer.Ordinal); if (!File.Exists(item4)) { goto IL_01bb; } if (new FileInfo(item4).Length > 8388608) { throw new InvalidDataException("Config exceeds 8 MiB."); } string text2 = Path.GetExtension(item4).ToLowerInvariant(); if (text2 == ".cfg" || text2 == ".ini") { foreach (KeyValuePair<string, string> item5 in Ini.Parse(File.ReadAllText(item4))) { sortedDictionary2[item5.Key] = item5.Value; } goto IL_01bb; } Add("config-file", text, FileHash(item4, force), "SHA256 " + FileHash(item4, force).Substring(0, 12)); goto end_IL_00c4; IL_01bb: if (configs.TryGetValue(item4, out SortedDictionary<string, string> value2)) { foreach (KeyValuePair<string, string> item6 in value2) { sortedDictionary2[item6.Key] = item6.Value; } } foreach (KeyValuePair<string, string> item7 in sortedDictionary2) { Add("config", text + "/" + item7.Key.Replace('\0', '/'), item7.Value, Ini.Display(item7.Key, item7.Value)); } end_IL_00c4:; } catch (Exception ex) { errors.Add("Config " + text + ": " + ex.Message); } } } string[] array = new string[3] { Paths.PluginPath, Path.Combine(Paths.BepInExRootPath, "patchers"), Path.Combine(Paths.BepInExRootPath, "core") }; foreach (string path in array) { if (Directory.Exists(path)) { foreach (string item8 in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).OrderBy<string, string>((string x) => x, StringComparer.OrdinalIgnoreCase)) { if (!Same(item8, ownConfig)) { string text3 = Path.GetExtension(item8).ToLowerInvariant(); if (!(text3 == ".log") && !(text3 == ".pdb") && !new string[6] { "readme.md", "changelog.md", "license", "license.txt", "manifest.json", "icon.png" }.Contains(Path.GetFileName(item8).ToLowerInvariant())) { string text4 = Logical(item8, Paths.BepInExRootPath); try { string text5 = FileHash(item8, force); entries.Add(new ManifestEntry { Category = "file", Key = text4, Digest = text5, Display = "SHA256 " + text5.Substring(0, 12) }); } catch (Exception ex2) { errors.Add("File " + text4 + ": " + ex2.Message); } } } } } } string p = Path.Combine(Paths.ManagedPath, "Assembly-CSharp.dll"); entries.Add(new ManifestEntry { Category = "runtime", Key = "Assembly-CSharp", Digest = FileHash(p, force), Display = "Game binary" }); } catch (Exception ex3) { errors.Add("Scan failed: " + ex3.Message); } Manifest manifest = new Manifest(); manifest.Entries = entries.ToArray(); manifest.Errors = errors.Take(200).ToArray(); manifest.Seal(); return manifest; }); void Add(string c, string k, string text, string display) { entries.Add(new ManifestEntry { Category = c, Key = k, Digest = Hash.Text(text), Display = display }); } } private string FileHash(string p, bool force) { FileInfo fileInfo = new FileInfo(p); if (!force && cache.TryGetValue(p, out (long, long, string) value) && value.Item1 == fileInfo.Length && value.Item2 == fileInfo.LastWriteTimeUtc.Ticks) { return value.Item3; } string text = Hash.File(p); cache[p] = (fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks, text); return text; } private static bool Same(string a, string b) { return ConfigPolicy.IsOwn(a, b); } private static string Logical(string path, string root) { return Path.GetRelativePath(root, path).Replace('\\', '/').ToLowerInvariant(); } } internal static class Access { private static readonly Dictionary<string, FieldInfo> fields = new Dictionary<string, FieldInfo>(); public static FieldInfo Field(Type t, string name) { string text = t.FullName + "." + name; if (!fields.TryGetValue(text, out FieldInfo value)) { Dictionary<string, FieldInfo> dictionary = fields; FieldInfo obj = AccessTools.Field(t, name) ?? throw new MissingFieldException(text); value = obj; dictionary[text] = obj; } return value; } public static T Read<T>(object o, string name) { return (T)Field(o.GetType(), name).GetValue(o); } public static void Set(object o, string name, object? value) { Field(o.GetType(), name).SetValue(o, value); } } internal sealed class StatSnapshot { private readonly List<Action> restore = new List<Action>(); public StatSnapshot(object instance) { FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo f in fields) { object value = f.GetValue(instance); Dictionary<string, int> di = value as Dictionary<string, int>; if (di != null) { KeyValuePair<string, int>[] copy = di.ToArray(); restore.Add(delegate { di.Clear(); KeyValuePair<string, int>[] array = copy; for (int j = 0; j < array.Length; j++) { KeyValuePair<string, int> keyValuePair = array[j]; di[keyValuePair.Key] = keyValuePair.Value; } }); continue; } Dictionary<string, string> ds = value as Dictionary<string, string>; if (ds != null) { KeyValuePair<string, string>[] copy2 = ds.ToArray(); restore.Add(delegate { ds.Clear(); KeyValuePair<string, string>[] array = copy2; for (int j = 0; j < array.Length; j++) { KeyValuePair<string, string> keyValuePair = array[j]; ds[keyValuePair.Key] = keyValuePair.Value; } }); continue; } List<string> ls = value as List<string>; if (ls != null) { string[] copy3 = ls.ToArray(); restore.Add(delegate { ls.Clear(); ls.AddRange(copy3); }); continue; } List<EnemySetup> le = value as List<EnemySetup>; if (le != null) { EnemySetup[] copy4 = le.ToArray(); restore.Add(delegate { le.Clear(); le.AddRange(copy4); }); } else if (value is Level) { restore.Add(delegate { f.SetValue(instance, value); }); } else if (!f.IsInitOnly && (f.FieldType.IsPrimitive || f.FieldType.IsEnum || f.FieldType == typeof(string))) { restore.Add(delegate { f.SetValue(instance, value); }); } } } public void Restore() { foreach (Action item in restore) { item(); } } } internal sealed class GameBridge { public RoundPlan? Plan; public readonly CosmeticTracker Cosmetics = new CosmeticTracker(); public readonly DeliveryTracker Deliveries = new DeliveryTracker(); public readonly HuntTracker Hunts = new HuntTracker(); private readonly Dictionary<ValuableObject, float> startingValues = new Dictionary<ValuableObject, float>(); private StatSnapshot? stats; private StatSnapshot? run; private StatSnapshot? progress; private SimulationMode physics; private float timeScale; private int normalMaxPlayers; private int normalPhotonMax; private State random; private readonly List<(NavMeshAgent agent, bool stopped)> agents = new List<(NavMeshAgent, bool)>(); public bool Active { get; private set; } public bool Protected => stats != null; public bool Frozen { get; private set; } public bool AllowStart { get; private set; } public static bool InNativeLobby { get { if ((Object)(object)RunManager.instance != (Object)null) { if (!SemiFunc.RunIsLobbyMenu()) { return SemiFunc.RunIsLobby(); } return true; } return false; } } public static bool CanOpen { get { if ((Object)(object)RunManager.instance != (Object)null && (Object)(object)StatsManager.instance != (Object)null && (Object)(object)GameManager.instance != (Object)null) { if (!SemiFunc.IsMainMenu() && !SemiFunc.RunIsLobbyMenu()) { return SemiFunc.RunIsLobby(); } return true; } return false; } } public static int Health { get { if (!((Object)(object)PlayerAvatar.instance == (Object)null)) { return Access.Read<int>(PlayerAvatar.instance.playerHealth, "health"); } return 0; } } public static int MaxHealth { get { if (!((Object)(object)PlayerAvatar.instance == (Object)null)) { return Access.Read<int>(PlayerAvatar.instance.playerHealth, "maxHealth"); } return 0; } } public static long Delivered => Plugin.Instance.Game.Deliveries.Ledger.Delivered; public static string[] Stages() { if (!((Object)(object)RunManager.instance == (Object)null)) { return (from l in RunManager.instance.levels where (Object)(object)l != (Object)null select ((Object)l).name).Distinct().OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray(); } return Array.Empty<string>(); } public static string[] Items() { StatsManager instance = StatsManager.instance; if ((Object)(object)instance == (Object)null) { return Array.Empty<string>(); } if (instance.itemDictionary.Count == 0) { Item[] array = Resources.LoadAll<Item>(instance.folderPath); foreach (Item val in array) { if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(((Object)val).name)) { instance.itemDictionary.TryAdd(((Object)val).name, val); } } } return (from x in instance.itemDictionary where (Object)(object)x.Value != (Object)null && !x.Value.disabled select x.Key).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray(); } public static string Pose(Transform t) { //IL_0029: 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) return ((Object)t).name.Replace("(Clone)", "") + "@" + Vector(t.position) + "/" + Vector(t.eulerAngles); } private static string Vector(Vector3 v) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) return string.Join(",", new float[3] { v.x, v.y, v.z }.Select((float x) => Math.Round(x, 3).ToString("F3", CultureInfo.InvariantCulture))); } public IEnumerator Load(RoundPlan plan, string self, Action<StageReady> ready, Action<string> error) { try { Prepare(plan, self); } catch (Exception ex) { error(ex.Message); yield break; } double deadline; if (plan.HasTeams && !plan.IsSpectator(self)) { bool failed = false; yield return Plugin.Instance.Teams.Enter(plan, delegate(string reason) { failed = true; error(reason); }); if (failed) { yield break; } } else { Plugin.Instance.Teams.Reset(); PhotonNetwork.Disconnect(); deadline = Transport.Now + 20.0; while (PhotonNetwork.IsConnected) { if (Transport.Now > deadline) { error("Photon disconnect timed out."); yield break; } yield return null; } } if ((Object)(object)GameDirector.instance != (Object)null) { GameDirector.instance.currentState = (gameState)0; } yield return SceneManager.LoadSceneAsync("Main"); deadline = Transport.Now + 180.0; while ((Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated) { if (!Active) { yield break; } if (Transport.Now > deadline) { error("Stage generation timed out."); yield break; } yield return null; } yield return (object)new WaitForSecondsRealtime(1f); if (plan.HasTeams && !plan.IsSpectator(self) && !PhotonNetwork.IsMasterClient) { deadline = Transport.Now + 90.0; while (!(((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"sr-stage"] is byte[] array) || array.Length == 0) { if (Transport.Now > deadline) { error("Waiting for the team stage timed out."); yield break; } yield return null; } } try { VerifyLocalAuthority(); ValuableObject[] array2 = Object.FindObjectsOfType<ValuableObject>(); if (array2.Any((ValuableObject v) => !Access.Read<bool>(v, "dollarValueSet"))) { throw new InvalidOperationException("Valuable prices are not ready."); } startingValues.Clear(); ValuableObject[] array3 = array2; foreach (ValuableObject val in array3) { startingValues.Add(val, Access.Read<float>(val, "dollarValueOriginal")); } VerifyStartingValuables(); Deliveries.Capture(array2); string[] first = (from x in Object.FindObjectsOfType<ItemAttributes>() where (Object)(object)x.item != (Object)null && plan.Items.Contains(((Object)x.item).name) select ((Object)x.item).name).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray(); string[] source = Enumerable.Range(0, (!plan.HasTeams || plan.IsSpectator(self)) ? 1 : plan.Teammates(self).Length).SelectMany((int _) => plan.Items).ToArray(); if (!first.SequenceEqual(source.OrderBy<string, string>((string x) => x, StringComparer.Ordinal))) { throw new InvalidOperationException("The generated item quantities do not match the host loadout."); } if (Health != 100 + 20 * plan.Loadout.Health || MaxHealth != Health) { throw new InvalidOperationException("Starting health does not match the host loadout."); } PlayerAvatar instance = PlayerAvatar.instance; PlayerController instance2 = PlayerController.instance; Loadout loadout = plan.Loadout; if (Access.Read<int>(instance2, "JumpExtra") != loadout.ExtraJump || !Mathf.Approximately(instance2.SprintSpeedUpgrades, (float)loadout.Speed) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeCrouchRest"), (float)loadout.CrouchRest) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeTumbleWings"), (float)loadout.TumbleWings) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeTumbleClimb"), (float)loadout.TumbleClimb) || Access.Read<int>(Access.Read<object>(instance, "tumble"), "tumbleLaunch") != loadout.TumbleLaunch) { throw new InvalidOperationException("Initial movement upgrades do not match the host loadout."); } Plugin.Log.LogInfo((object)string.Format("Initial upgrades verified: Lv.{0}, {1}, seed={2}, counts={3}, health={4}, stamina={5}", plan.Level, plan.LevelName, plan.Seed, string.Join(",", Upgrades.Values(loadout)), Health, instance2.EnergyStart)); VerifyStartingStation(); int num2 = Access.Read<int>(RoundDirector.instance, "extractionPoints"); if (num2 < 1) { throw new InvalidOperationException("Extraction points are not ready."); } List<string> list = new List<string>(); list.Add(plan.RecordKey); list.Add("extraction-count:" + num2); list.Add("health:" + Health); list.Add("upgrades:" + string.Join(",", Upgrades.Values(loadout))); list.Add("charging-station:" + Pose(((Component)ChargingStation.instance).transform) + ":" + 30 + "/" + 3); List<string> list2 = list; Module[] array4 = Object.FindObjectsOfType<Module>(); foreach (Module val2 in array4) { list2.Add("module:" + Pose(((Component)val2).transform)); } array3 = array2; foreach (ValuableObject val3 in array3) { list2.Add("value:" + Pose(((Component)val3).transform) + ":" + Access.Read<float>(val3, "dollarValueOriginal").ToString("R", CultureInfo.InvariantCulture) + "/" + Access.Read<float>(val3, "dollarValueCurrent").ToString("R", CultureInfo.InvariantCulture)); } EnemyParent[] array5 = Object.FindObjectsOfType<EnemyParent>(); foreach (EnemyParent val4 in array5) { list2.Add("enemy:" + Pose(((Component)val4).transform)); } ExtractionPoint[] array6 = Object.FindObjectsOfType<ExtractionPoint>(); foreach (ExtractionPoint val5 in array6) { list2.Add("extraction:" + Pose(((Component)val5).transform)); } if (!plan.HasTeams || plan.IsSpectator(self)) { ItemAttributes[] array7 = Object.FindObjectsOfType<ItemAttributes>(); foreach (ItemAttributes val6 in array7) { list2.Add("item:" + ((Object)val6.item).name + ":" + Pose(((Component)val6).transform)); } } if (!plan.HasTeams || plan.IsSpectator(self)) { PhysGrabObject[] array8 = Object.FindObjectsOfType<PhysGrabObject>(); foreach (PhysGrabObject val7 in array8) { list2.Add("physics:" + Pose(((Component)val7).transform)); Rigidbody component = ((Component)val7).GetComponent<Rigidbody>(); if ((Object)(object)component != (Object)null) { list2.Add("mass:" + Pose(((Component)val7).transform) + ":" + component.mass.ToString("R", CultureInfo.InvariantCulture)); } } } ItemBattery[] array9 = Object.FindObjectsOfType<ItemBattery>(); foreach (ItemBattery val8 in array9) { if (Math.Abs(val8.batteryLife - 100f) > 0.001f) { throw new InvalidOperationException("An initial item battery is not full."); } if (!plan.HasTeams || plan.IsSpectator(self)) { list2.Add("battery:" + Pose(((Component)val8).transform) + ":" + val8.batteryLife.ToString("R", CultureInfo.InvariantCulture)); } } list2.AddRange(Cosmetics.Capture()); if (!plan.HasTeams || plan.IsSpectator(self) || PhotonNetwork.IsMasterClient) { list2.AddRange(Hunts.Capture()); Plugin.Log.LogInfo((object)("Cosmetic boxes captured: " + Cosmetics.Initial.Length + "; full-health bonus: " + Cosmetics.ReferenceWeight * 50)); if (!plan.HasTeams || plan.IsSpectator(self)) { list2.Add("spawn:" + Pose(((Component)PlayerAvatar.instance).transform)); } list2.Add("goal:" + Access.Read<int>(RoundDirector.instance, "haulGoal") + ":" + Access.Read<int>(RoundDirector.instance, "haulGoalMax")); long num3 = array2.Sum((ValuableObject v) => (long)Access.Read<float>(v, "dollarValueOriginal")); if (num3 <= 0) { throw new InvalidOperationException("No initial valuable value."); } StageReady stageReady = new StageReady { Hash = Hash.Fields(list2.OrderBy<string, string>((string x) => x, StringComparer.Ordinal)), Value = num3, Health = MaxHealth, ExtractionPoints = num2, CosmeticBoxes = Cosmetics.Initial, EnemyGroups = Hunts.Initial }; if (plan.HasTeams && !plan.IsSpectator(self)) { Room currentRoom = PhotonNetwork.CurrentRoom; Hashtable val9 = new Hashtable(); ((Dictionary<object, object>)val9).Add((object)"sr-stage", (object)CheckpointPayload.Pack(Codec.Write(new TeamStage { Stage = stageReady, Hunts = Hunts.Snapshot() }))); currentRoom.SetCustomProperties(val9, (Hashtable)null, (WebFlags)null); } ready(stageReady); } else { TeamStage teamStage = Codec.Read<TeamStage>(CheckpointPayload.Unpack((byte[])((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"sr-stage"])); Hunts.Restore(teamStage.Hunts); if (teamStage.Stage.Value != array2.Sum((ValuableObject v) => (long)Access.Read<float>(v, "dollarValueOriginal")) || teamStage.Stage.Health != MaxHealth || teamStage.Stage.ExtractionPoints != num2) { throw new InvalidOperationException("Team stage values differ."); } ready(teamStage.Stage); } } catch (Exception ex2) { error(ex2.Message); } } public void Observe(RoundPlan plan) { CaptureNormalState(); Active = true; Frozen = true; AllowStart = false; Plan = plan; agents.Clear(); Cosmetics.Clear(); Deliveries.Clear(); Hunts.Clear(); startingValues.Clear(); Physics.simulationMode = (SimulationMode)2; Time.timeScale = 1f; Plugin.Instance.Teams.Reset(); NetworkConnect[] array = Object.FindObjectsOfType<NetworkConnect>(); foreach (NetworkConnect obj in array) { ((MonoBehaviour)obj).StopAllCoroutines(); Object.Destroy((Object)(object)((Component)obj).gameObject); } PhotonNetwork.AutomaticallySyncScene = false; PhotonNetwork.Disconnect(); GameManager.instance.SetGameMode(0); HUD instance = HUD.instance; if (instance != null) { instance.Show(); } if ((Object)(object)MenuManager.instance != (Object)null) { MenuManager.instance.PageCloseAll(); } } public void OpenPrivateLobby() { if (Active || InNativeLobby) { return; } if (!CanOpen || (Object)(object)MainMenuOpen.instance == (Object)null) { throw new InvalidOperationException("Private lobby must open from the main menu."); } CaptureNormalState(); try { GameManager.instance.SetMaxPlayers(150); GameManager.instance.localTest = false; RunManager.instance.ResetProgress(); GameManager.instance.SetConnectRandom(false); GameManager.instance.SetLobbyType((LobbyTypes)0); StatsManager instance = StatsManager.instance; Access.Set(instance, "saveFileCurrent", ""); Access.Set(instance, "saveFileReady", false); instance.teamName = "ScoreRace"; instance.teamNameChanged = true; AccessTools.Method(typeof(StatsManager), "PlayersAddAll", (Type[])null, (Type[])null).Invoke(instance, null); Access.Set(RunManager.instance, "waitToChangeScene", true); RunManager.instance.ChangeLevel(true, false, (ChangeLevelType)3); MainMenuOpen.instance.NetworkConnect(); } catch { Restore(); throw; } } public void CaptureNormalState() { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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) if (!Protected) { stats = new StatSnapshot(StatsManager.instance); run = new StatSnapshot(RunManager.instance); if ((Object)(object)ProgressionManager.instance != (Object)null) { progress = new StatSnapshot(ProgressionManager.instance); } physics = Physics.simulationMode; timeScale = Time.timeScale; random = Random.state; normalMaxPlayers = GameManager.instance.maxPlayers; normalPhotonMax = GameManager.instance.maxPlayersPhoton; } } private void Prepare(RoundPlan plan, string self) { if (!Stages().Contains(plan.LevelName) || plan.Items.Any((string x) => !Items().Contains(x)) || !Items().Contains("Item Power Crystal")) { throw new InvalidOperationException("Host stage/item catalog is unavailable."); } CaptureNormalState(); Active = true; Frozen = true; AllowStart = false; Plan = plan; agents.Clear(); Cosmetics.Clear(); Deliveries.Clear(); Hunts.Clear(); startingValues.Clear(); Physics.simulationMode = (SimulationMode)2; Time.timeScale = 1f; GameManager.instance.SetGameMode((plan.HasTeams && !plan.IsSpectator(self)) ? 1 : 0); StatsManager instance = StatsManager.instance; FieldInfo[] fields = typeof(StatsManager).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.GetValue(instance) is Dictionary<string, int> dictionary && fieldInfo.Name != "stripTheseDictionaries") { dictionary.Clear(); } } Access.Read<List<string>>(instance, "takenItemNames").Clear(); Access.Set(instance, "saveFileReady", false); instance.runStats["level"] = plan.Level - 1; instance.runStats["currency"] = 0; instance.runStats["lives"] = 1; instance.runStats["chargingStationCharge"] = 3; instance.runStats["chargingStationChargeTotal"] = 30; instance.runStats["totalHaul"] = 0; string[] array = plan.Teammates(self); foreach (string key in array) { instance.playerUpgradeHealth[key] = plan.Loadout.Health; instance.playerUpgradeStamina[key] = plan.Loadout.Stamina; instance.playerUpgradeSpeed[key] = plan.Loadout.Speed; instance.playerUpgradeStrength[key] = plan.Loadout.Strength; instance.playerUpgradeRange[key] = plan.Loadout.Range; instance.playerHealth[key] = 100 + plan.Loadout.Health * 20; instance.playerUpgradeCrouchRest[key] = plan.Loadout.CrouchRest; instance.playerUpgradeExtraJump[key] = plan.Loadout.ExtraJump; instance.playerUpgradeLaunch[key] = plan.Loadout.TumbleLaunch; instance.playerUpgradeTumbleWings[key] = plan.Loadout.TumbleWings; instance.playerUpgradeTumbleClimb[key] = plan.Loadout.TumbleClimb; } foreach (IGrouping<string, string> item in from x in plan.Items group x by x) { instance.itemsPurchased[item.Key] = item.Count() * ((!plan.HasTeams || plan.IsSpectator(self)) ? 1 : plan.Teammates(self).Length); instance.itemsPurchasedTotal[item.Key] = instance.itemsPurchased[item.Key]; } instance.itemsPurchased["Item Power Crystal"] = 3; instance.itemsPurchasedTotal["Item Power Crystal"] = 3; ItemManager.instance.ResetAllItems(); ItemManager.instance.spawnedItems.Clear(); ItemManager.instance.localPlayerInventory.Clear(); RunManager instance2 = RunManager.instance; Access.Set(instance2, "runLives", 1); Access.Set(instance2, "runStarted", false); instance2.levelCurrent = instance2.levels.First((Level x) => ((Object)x).name == plan.LevelName); instance2.levelsCompleted = plan.Level - 1; array = new string[9] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "levelFailed", "gameOver", "allPlayersDead", "masterSwitched", "levelIsShop" }; foreach (string name in array) { Access.Set(instance2, name, false); } Access.Set(instance2, "levelPrevious", instance2.levelCurrent); Access.Set(instance2, "previousRunLevel", instance2.levelCurrent); Access.Set(instance2, "loadLevel", 0); Access.Set(instance2, "saveLevel", 0); Access.Set(instance2, "cosmeticWorldObjectCooldown", 0f); Access.Set(instance2, "cosmeticWorldObjectBadLuckCount", 0); instance2.enemiesSpawned.Clear(); Access.Read<List<EnemySetup>>(instance2, "enemiesSpawnedToDelete").Clear(); instance2.UpdateMoonLevel(); Determinism.Reset(); } public void Warmup() { if (Active) { AllowStart = true; GameDirector.instance.SetStart(); } } public void HoldAgents() { if (!Active || !Frozen) { return; } NavMeshAgent[] array = Object.FindObjectsOfType<NavMeshAgent>(); foreach (NavMeshAgent a in array) { if (((Behaviour)a).enabled && a.isOnNavMesh && !agents.Any<(NavMeshAgent, bool)>(((NavMeshAgent agent, bool stopped) x) => (Object)(object)x.agent == (Object)(object)a)) { agents.Add((a, a.isStopped)); a.isStopped = true; } } Physics.simulationMode = (SimulationMode)2; } private void VerifyStartingValuables() { foreach (KeyValuePair<ValuableObject, float> startingValue in startingValues) { ValuableObject key = startingValue.Key; if ((Object)(object)key == (Object)null || !Access.Read<bool>(key, "dollarValueSet") || !Rules.Finite(startingValue.Value) || startingValue.Value < 0f || Access.Read<float>(key, "dollarValueOriginal") != startingValue.Value || Access.Read<float>(key, "dollarValueCurrent") != startingValue.Value) { throw new InvalidOperationException("A starting valuable was damaged, removed or changed before the race."); } } } private void VerifyStartingStation() { ChargingStation instance = ChargingStation.instance; if ((Object)(object)instance == (Object)null || Access.Read<int>(instance, "chargeTotal") != 30 || Access.Read<int>(instance, "chargeInt") != 3 || !Mathf.Approximately(Access.Read<float>(instance, "chargeFloat"), 0.3f)) { throw new InvalidOperationException("The charging station must start at 30% with three installed crystals."); } } public static void VerifyLocalAuthority() { if (Plugin.Instance.Teams.Enabled) { if ((Object)(object)GameManager.instance == (Object)null || GameManager.instance.gameMode != 1 || !PhotonNetwork.InRoom) { throw new InvalidOperationException("The team stage connection was lost."); } } else if ((Object)(object)GameManager.instance == (Object)null || GameManager.instance.gameMode != 0 || PhotonNetwork.IsConnected) { throw new InvalidOperationException("The race stage must run in a disconnected local simulation."); } } public void Release() { //IL_0171: Unknown result type (might be due to invalid IL or missing references) VerifyLocalAuthority(); VerifyStartingValuables(); VerifyStartingStation(); float[] array = (from v in startingValues.Keys select ((Component)v).GetComponent<PhysGrabObjectImpactDetector>() into d where (Object)(object)d != (Object)null select Access.Read<float>(d, "indestructibleSpawnTimer")).ToArray(); Plugin.Log.LogInfo((object)("Valuable start verified: " + startingValues.Count + " full value; minimum native spawn protection=" + ((array.Length != 0) ? array.Min() : 0f).ToString("F2", CultureInfo.InvariantCulture) + "s.")); foreach (var agent in agents) { if ((Object)(object)agent.agent != (Object)null && ((Behaviour)agent.agent).enabled && agent.agent.isOnNavMesh) { agent.agent.isStopped = agent.stopped; } } agents.Clear(); Physics.simulationMode = physics; Frozen = false; Plugin.Log.LogInfo((object)("Native menu key: " + InputManager.instance.GetKeyString((InputKey)6))); } public void ResumeSimulation() { //IL_0074: Unknown result type (might be due to invalid IL or missing references) foreach (var agent in agents) { if ((Object)(object)agent.agent != (Object)null && ((Behaviour)agent.agent).enabled && agent.agent.isOnNavMesh) { agent.agent.isStopped = agent.stopped; } } agents.Clear(); Physics.simulationMode = physics; Frozen = false; } public void Freeze() { if (Active) { Frozen = true; Physics.simulationMode = (SimulationMode)2; } } public void PurchaseItems() { ItemManager instance = ItemManager.instance; instance.purchasedItems.Clear(); foreach (string item in Enumerable.Range(0, (!Plan.HasTeams || Plan.IsSpectator(Plugin.Instance.Race.Net.Self)) ? 1 : Plan.Teammates(Plugin.Instance.Race.Net.Self).Length).SelectMany((int _) => Plan.Items)) { instance.purchasedItems.Add(StatsManager.instance.itemDictionary[item]); } } public void SpawnItems() { //IL_017b: 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_0158: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Invalid comparison between Unknown and I4 //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_0232: Unknown result type (might be due to invalid IL or missing references) //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: 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) ItemManager instance = ItemManager.instance; if (Plan.HasTeams && !Plan.IsSpectator(Plugin.Instance.Race.Net.Self) && !PhotonNetwork.IsMasterClient) { return; } ItemVolume[] array = instance.itemVolumes.OrderBy<ItemVolume, string>((ItemVolume v) => Pose(((Component)v).transform), StringComparer.Ordinal).ToArray(); Dictionary<ItemVolume, int> counts = new Dictionary<ItemVolume, int>(); instance.spawnedItems.Clear(); foreach (Item item in instance.purchasedItems) { ItemVolume[] array2 = array.Where((ItemVolume v) => v.itemVolume == item.itemVolume).ToArray(); if (array2.Length == 0) { throw new InvalidOperationException("No truck volume for " + ((Object)item).name); } int value2; ItemVolume val = array2.OrderBy((ItemVolume v) => counts.TryGetValue(v, out value2) ? value2 : 0).First(); int value; int num = (counts.TryGetValue(val, out value) ? value : 0); counts[val] = num + 1; Vector3 val2 = ((num == 0) ? Vector3.zero : (((Component)val).transform.right * (((int)item.itemType == 2) ? 2.2f : 0.65f) * (float)num)); if (Plan.HasTeams && !Plan.IsSpectator(Plugin.Instance.Race.Net.Self)) { PhotonNetwork.InstantiateRoomObject(((PrefabRef<GameObject>)(object)item.prefab).ResourcePath, ((Component)val).transform.position + val2, ((Component)val).transform.rotation * item.spawnRotationOffset, (byte)0, (object[])null); } else { Object.Instantiate<GameObject>(((PrefabRef<GameObject>)(object)item.prefab).Prefab, ((Component)val).transform.position + val2, ((Component)val).transform.rotation * item.spawnRotationOffset); } } ItemVolume[] array3 = array; for (int num2 = 0; num2 < array3.Length; num2++) { Object.Destroy((Object)(object)((Component)array3[num2]).gameObject); } instance.itemVolumes.Clear(); } public IEnumerator ReturnToLobby(Action done, Action<string> error) { if (PhotonNetwork.IsConnected) { PhotonNetwork.Disconnect(); while (PhotonNetwork.IsConnected) { yield return null; } } Plugin.Instance.Teams.Reset(); Active = false; Frozen = false; AllowStart = false; Plan = null; agents.Clear(); Cosmetics.Clear(); Deliveries.Clear(); Hunts.Clear(); startingValues.Clear(); Physics.simulationMode = physics; Time.timeScale = 1f; GameManager.instance.SetMaxPlayers(150); GameManager.instance.SetGameMode(0); GameManager.instance.localTest = false; GameManager.instance.SetConnectRandom(false); GameManager.instance.SetLobbyType((LobbyTypes)0); RunManager.instance.ResetProgress(); Access.Set(StatsManager.instance, "saveFileCurrent", ""); Access.Set(StatsManager.instance, "saveFileReady", false); StatsManager.instance.teamName = "ScoreRace"; StatsManager.instance.teamNameChanged = true; AccessTools.Method(typeof(StatsManager), "PlayersAddAll", (Type[])null, (Type[])null).Invoke(StatsManager.instance, null); RunManager.instance.levelCurrent = RunManager.instance.levelLobbyMenu; string[] array = new string[6] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "allPlayersDead", "gameOver" }; foreach (string name in array) { Access.Set(RunManager.instance, name, false); } if ((Object)(object)GameDirector.instance != (Object)null) { GameDirector.instance.currentState = (gameState)0; } yield return SceneManager.LoadSceneAsync("LobbyJoin"); double deadline = Transport.Now + 90.0; while (!PhotonNetwork.InRoom) { if (Transport.Now > deadline) { error("Reconnecting to the native lobby timed out."); yield break; } yield return null; } if (PhotonNetwork.IsMasterClient) { GameManager.instance.SetGameMode(1); PhotonNetwork.LoadLevel("Main"); } while (!InNativeLobby || !PhotonNetwork.InRoom || (Object)(object)MenuPageLobby.instance == (Object)null) { if (Transport.Now > deadline) { error("Returning to the native lobby timed out."); yield break; } yield return null; } if (PhotonNetwork.IsMasterClient) { SteamManager.instance.UnlockLobby(false); } done(); } public IEnumerator Exit(Action done) { if (!Active) { Restore(); done(); yield break; } Frozen = true; AllowStart = false; PhotonNetwork.Disconnect(); while (PhotonNetwork.IsConnected) { yield return null; } Plugin.Instance.Teams.Reset(); if ((Object)(object)SteamManager.instance != (Object)null) { SteamManager.instance.LeaveLobby(); } GameManager.instance.SetGameMode(0); RunManager.instance.levelCurrent = RunManager.instance.levelMainMenu; string[] array = new string[6] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "allPlayersDead", "gameOver" }; foreach (string name in array) { Access.Set(RunManager.instance, name, false); } if ((Object)(object)GameDirector.instance != (Object)null) { GameDirector.instance.currentState = (gameState)0; } yield return SceneManager.LoadSceneAsync("Main"); yield return null; Restore(); RunManager.instance.levelCurrent = RunManager.instance.levelMainMenu; done(); } public void Restore() { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) if (Protected || Active) { stats?.Restore(); run?.Restore(); progress?.Restore(); stats = null; run = null; progress = null; if ((Object)(object)GameManager.instance != (Object)null) { GameManager.instance.maxPlayers = normalMaxPlayers; GameManager.instance.maxPlayersPhoton = normalPhotonMax; } Physics.simulationMode = physics; Time.timeScale = timeScale; Random.state = random; Active = false; Frozen = false; AllowStart = false; Plan = null; agents.Clear(); Cosmetics.Clear(); Deliveries.Clear(); Hunts.Clear(); startingValues.Clear(); } } } internal sealed class TeamStage { public StageReady Stage = new StageReady(); public HuntSnapshot Hunts = new HuntSnapshot(); } internal sealed class HuntTracker { internal sealed class Spawn { public EnemySetup Setup; public readonly List<EnemyParent> Parents = new List<EnemyParent>(); } private readonly Stack<Spawn> spawning = new Stack<Spawn>(); private readonly List<EnemyGroup> groups = new List<EnemyGroup>(); private readonly Dictionary<int, string> members = new Dictionary<int, string>(); private readonly HashSet<int> observedDeaths = new HashSet<int>(); private readonly Dictionary<string, int> lives = new Dictionary<string, int>(); public HuntLedger Ledger = new HuntLedger(Array.Empty<EnemyGroup>()); public EnemyGroup[] Initial => groups.ToArray(); public void Clear() { spawning.Clear(); groups.Clear(); members.Clear(); observedDeaths.Clear(); lives.Clear(); Ledger = new HuntLedger(Array.Empty<EnemyGroup>()); } public HuntSnapshot Snapshot() { return new HuntSnapshot { Groups = Initial, Members = new Dictionary<int, string>(members), Deaths = observedDeaths.ToArray(), Lives = new Dictionary<string, int>(lives), Killed = Ledger.Killed, Observed = Ledger.ObservedLives }; } public void Restore(HuntSnapshot s) { if (!Hunts.ValidGroups(s.Groups) || s.Members.Count > 1024 || s.Deaths.Length > 1024 || s.Lives.Count > 1024) { throw new InvalidOperationException("Invalid team hunt checkpoint."); } Clear(); groups.AddRange(s.Groups); foreach (KeyValuePair<int, string> member in s.Members) { members[member.Key] = member.Value; } int[] deaths = s.Deaths; foreach (int item in deaths) { observedDeaths.Add(item); } foreach (KeyValuePair<string, int> life in s.Lives) { lives[life.Key] = life.Value; } Ledger = new HuntLedger(Initial); Ledger.Restore(s.Killed, s.Observed); } public Spawn Begin(EnemySetup setup) { Spawn spawn = new Spawn { Setup = setup }; spawning.Push(spawn); return spawn; } public void Created(EnemyParent parent) { if (spawning.Count > 0) { spawning.Peek().Parents.Add(parent); } } public void End(Spawn s, bool succeeded) { if (spawning.Count == 0 || spawning.Pop() != s) { throw new InvalidOperationException("Enemy group spawn scope changed."); } if (!succeeded || s.Parents.Count == 0) { return; } EnemyDirector instance = EnemyDirector.instance; int tier = (instance.enemiesDifficulty3.Contains(s.Setup) ? 3 : (instance.enemiesDifficulty2.Contains(s.Setup) ? 2 : (instance.enemiesDifficulty1.Contains(s.Setup) ? 1 : s.Parents.Max((EnemyParent e) => e.difficulty + 1)))); string text = Hash.Text("enemy-group:" + groups.Count + ":" + ((Object)s.Setup).name); List<string> list = new List<string>(); for (int num = 0; num < s.Parents.Count; num++) { EnemyParent val = s.Parents[num]; string text2 = Hash.Text(text + ":" + num + ":" + ((Object)val).name.Replace("(Clone)", "")); list.Add(text2); EnemyHealth[] componentsInChildren = ((Component)val).GetComponentsInChildren<EnemyHealth>(true); foreach (EnemyHealth obj in componentsInChildren) { members.Add(TeamRuntime.ObjectId((Component)(object)obj), text2); } } groups.Add(new EnemyGroup { Id = text, Tier = tier, Members = list.ToArray() }); } public IEnumerable<string> Capture() { if (spawning.Count != 0) { throw new InvalidOperationException("An enemy group has not finished spawning."); } Ledger = new HuntLedger(Initial); return groups.Select((EnemyGroup g) => "enemy-group:" + g.Id + ":" + g.Tier + ":" + string.Join(",", g.Members)); } public void Spawned(EnemyHealth health) { int num = TeamRuntime.ObjectId((Component)(object)health); if (!Access.Read<bool>(health, "dead") && observedDeaths.Remove(num) && members.TryGetValue(num, out string value)) { lives.TryGetValue(value, out var value2); lives[value] = value2 + 1; } } public void Defeated(EnemyHealth health) { int num = TeamRuntime.ObjectId((Component)(object)health); if (Access.Read<bool>(health, "dead") && members.TryGetValue(num, out string value) && observedDeaths.Add(num)) { lives.TryGetValue(value, out var value2); Ledger.Defeat(value, value2); } } } internal sealed class HuntSnapshot { public EnemyGroup[] Groups = Array.Empty<EnemyGroup>(); public Dictionary<int, string> Members = new Dictionary<int, string>(); public int[] Deaths = Array.Empty<int>(); public Dictionary<string, int> Lives = new Dictionary<string, int>(); public string[] Killed = Array.Empty<string>(); public string[] Observed = Array.Empty<string>(); } internal sealed class ObserverRoster { public string Round = ""; public string[] Players = Array.Empty<string>(); } internal sealed class ObserverRequest { public string Round = ""; public string Environment = ""; } internal sealed class ObserverSnapshot { public RoundPlan Plan = new RoundPlan(); public StageReady Initial = new StageReady(); public double Elapsed; public string[] Observers = Array.Empty<string>(); public string[] Departed = Array.Empty<string>(); public bool Finished; } internal sealed class Race { private sealed class SavedRecord { public Result Result = new Result(); } private sealed class ManifestMessage { public Manifest Manifest = new Manifest(); public string Name = ""; } private readonly HashSet<string> lateSpectators = new HashSet<string>(); private readonly Dictionary<string, Queue<Result>> observerHistory = new Dictionary<string, Queue<Result>>(); private int historyTurn; private string observerOffer = ""; private string lastObserverRoster = ""; private double nextObserverRequest; private double nextObserverRoster; private readonly Plugin plugin; private readonly EnvironmentScan scanner; public readonly Transport Net = new Transport(); public Phase State; public string Session = ""; public string Status = "Use Singleplayer or Start in the standard lobby."; public string HostSettings = ""; public PeerRow[] Rows = Array.Empty<PeerRow>(); public RoundPlan? Plan; public TeamDefinition[] LobbyTeams = Array.Empty<TeamDefinition>(); public string[] LobbySpectators = Array.Empty<string>(); private readonly HashSet<string> roleKnown = new HashSet<string>(); private readonly Dictionary<string, int> teamChoices = new Dictionary<string, int>(); public StageReady? Initial; public readonly Dictionary<string, Result> Results = new Dictionary<string, Result>(); public bool DeathObserved; public long Damage; public readonly ResultNavigation Navigation = new ResultNavigation(); public readonly ResultPages Pages = new ResultPages(); private readonly HashSet<string> departed = new HashSet<string>(); private bool returningToLobby; private bool startCommitted; private ResultWait? resultWait; private int resultDisplayFrame = -1; private bool ownResultShown; private bool shownAcknowledged; private readonly HashSet<string> resultAcks = new HashSet<string>(); private double nextResultSend; private double nextNetworkWarning; private ResultBarrier? transition; private ResultAction pendingAction; public string Best = ""; private readonly LobbyStartRequest startRequest = new LobbyStartRequest(); private string lastLobbyDiagnostic = ""; private readonly Dictionary<string, Manifest> manifests = new Dictionary<string, Manifest>(); private readonly Dictionary<string, string> names = new Dictionary<string, string>(); private readonly Dictionary<string, bool> votes = new Dictionary<string, bool>(); private readonly Dictionary<string, StageReady> stages = new Dictionary<string, StageReady>(); private readonly Dictionary<string, double> seen = new Dictionary<string, double>(); private readonly HashSet<string> acknowledgements = new HashSet<string>(); private readonly HashSet<string> armed = new HashSet<string>(); private readonly Dictionary<string, string> saved = new Dictionary<string, string>(); private Manifest? local; private Task<Manifest>? scan; private Action? afterScan; private double nextBeat; private double nextScan; private double phaseTime; private double startLocal; private double lastElapsed; private double offset; private double bestRtt = double.MaxValue; private int clockSamples; private int lastHealth; private bool exiting; private bool waitingReady; private bool wasNativeLobby; private double nextAutoReady; private string sessionLobby = ""; private string roster = ""; private string lastLobbyPayload = ""; private double nextLobbyPublish; private double lastLobbyPublish; public string[] Participants { get { if (Plan != null) { return LateJoin.Participants(Plan, lateSpectators, Net.Members); } return Net.Members; } } public bool AcceptingSpectators { get { if (Multiplayer && startCommitted && Plan != null && Initial != null && State >= Phase.Countdown) { return State <= Phase.Finished; } return false; } } public bool Observing => IsSpectator(Net.Self); public RaceOptions LobbyOptions { get { try { return Codec.Read<RaceOptions>(HostSettings); } catch { return new RaceOptions(); } } } public bool LocalDead { get { if (!Observing) { RoundPlan? plan = Plan; if (plan == null || !plan.HasTeams) { return DeathObserved; } return plugin.Teams.LocalDead; } return true; } } public bool LocalEliminated { get { if (!Observing) { RoundPlan? plan = Plan; if (plan == null || !plan.HasTeams) { return DeathObserved; } if (!plugin.Teams.Lives.Eliminated) { return DeathObserved; } return true; } return true; } } public long DisplayDamage { get { RoundPlan? plan = Plan; if (plan == null || !plan.HasTeams) { return Damage; } return plugin.Teams.Damage; } } public string RankId { get { if (!Observing) { RoundPlan? plan = Plan; if (plan != null && plan.HasTeams) { return (from r in Results.Values.Where((Result r) => r.TeamNumber == Plan.Team(Net.Self).Number).OrderBy((Result r) => r, Comparer<Result>.Create(Result.Compare)) select r.PlayerId).FirstOrDefault() ?? Net.Self; } } return Net.Self; } } public double? MatchDeadline => MatchScoring.Deadline(Plan, Results.Values); public double Elapsed { get { if (State != Phase.Running) { return lastElapsed; } return Math.Max(0.0, Transport.Now - startLocal); } } public bool Host => Net.Host == Net.Self; public bool Busy => scan != null; public bool HasSession => Session.Length > 0; public bool FinalResults { get { if (Plan != null) { return Plan.Competitors.All((string id) => Results.ContainsKey(id) || departed.Contains(id)); } return false; } } public bool Multiplayer { get; private set; } private string[] RemainingMembers => Net.Members.Where((string id) => (!startCommitted || IsParticipant(id)) && (!departed.Contains(id) || lateSpectators.Contains(id))).ToArray(); private bool HostResultReady { get { if (FinalResults) { if (Multiplayer && Host) { return resultWait?.Ready(Transport.Now) ?? false; } return true; } return false; } } public string ResultInstruction { get { if (!Multiplayer || Host || (Net.Members.Contains(Net.Host) && (!seen.TryGetValue(Net.Host, out var value) || !(Transport.Now - value > 45.0)))) { if (FinalResults) { if (Host && Multiplayer) { ResultWait? obj = resultWait; if (obj == null || !obj.AllPresented) { return "Waiting for all result screens to appear. You can change your selection."; } } if (!Host || !Multiplayer || HostResultReady) { if (Host || !Multiplayer) { return "Choose what to do next."; } return "Waiting for the host. MAIN MENU leaves immediately."; } return ((Navigation.Selection == ResultAction.None) ? "Choose what to do next." : ("Selected: " + ActionName(Navigation.Selection))) + " | " + Math.Ceiling(resultWait.Remaining(Transport.Now)) + "s\nYou can change your selection."; } if (Navigation.Selection != ResultAction.None) { return "Selected: " + ActionName(Navigation.Selection) + " - waiting for remaining players.\nYou can change your selection."; } return "Waiting for the remaining players."; } return "Host unavailable. Your result is saved locally. MAIN MENU leaves immediately."; } } public bool InRound { get { if (State >= Phase.Preparing && State <= Phase.Finished) { return !FinalResults; } return false; } } public bool ReadySelf => Rows.Any((PeerRow x) => x.Id == Net.Self && x.Ready); public bool SetupInvalid { get; private set; } public bool StartRequested => startRequest.Pending; private string SettingsHash => Hash.Text(HostSettings); public bool CanStart { get { bool value; Manifest value2; if (Host && State == Phase.Lobby && HostSettings.Length > 0 && Net.Members.Length != 0 && Net.Members.Length <= 150 && Net.Members.Any((string id) => !LobbySpectators.Contains(id)) && (Net.LobbyId.Length == 0 || Teams.Valid(LobbyTeams, Net.Members.Where((string id) => !LobbySpectators.Contains(id)).ToArray()))) { return Net.Members.All((string id) => votes.TryGetValue(id, out value) && value && manifests.TryGetValue(id, out value2) && local != null && value2.Digest == local.Digest && value2.Verify()); } return false; } } public double Countdown => Math.Max(0.0, startLocal - Transport.Now); public bool IsSpectator(string id) { RoundPlan? plan = Plan; if (plan == null || !plan.IsSpectator(id)) { return lateSpectators.Contains(id); } return true; } public bool IsParticipant(string id) { RoundPlan? plan = Plan; if (plan == null || !plan.Players.Contains(id)) { return lateSpectators.Contains(id); } return true; } public void OpenSpectatorLobby() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) if (Host && AcceptingSpectators && !((Object)(object)SteamManager.instance == (Object)null)) { Lobby val = Access.Read<Lobby>(SteamManager.instance, "currentLobby"); SteamId id = ((Lobby)(ref val)).Id; if (((SteamId)(ref id)).IsValid && SteamClient.IsValid && ((Lobby)(ref val)).IsOwnedBy(SteamClient.SteamId)) { ((Lobby)(ref val)).SetJoinable(true); } } } private void ResetObservers() { observerHistory.Clear(); historyTurn = 0; lateSpectators.Clear(); observerOffer = (lastObserverRoster = ""); nextObserverRequest = (nextObserverRoster = 0.0); } private void ApplyObservers(string[] observers) { foreach (string item2 in observers.Where((string id) => !lateSpectators.Contains(id))) { resultAcks.Remove(item2); } lateSpectators.Clear(); string[] array = observers; foreach (string item in array) { lateSpectators.Add(item); } LobbySpectators = LobbySpectators.Concat(observers).Distinct().ToArray(); array = observers; foreach (string text in array) { roleKnown.Add(text); teamChoices.Remove(text); } if (Plan != null) { Net.PinnedPeers = Plan.Players.Concat(observers).Distinct().ToArray(); } } private void PublishObservers(bool force = false) { if (Host && AcceptingSpectators) { string[] array = lateSpectators.Where(((IEnumerable<string>)Net.Members).Contains<string>).OrderBy<string, string>((string id) => id, StringComparer.Ordinal).ToArray(); ApplyObservers(array); string text = Plan.Id + ":" + string.Join("|", array); if (force || !(text == lastObserverRoster) || !(Transport.Now < nextObserverRoster)) { lastObserverRoster = text; nextObserverRoster = Transport.Now + 5.0; Broadcast("observers", new ObserverRoster { Round = Plan.Id, Players = array }); } } } private void ObserverTick(double now) { if (AcceptingSpectators && Host) { OpenSpectatorLobby(); PublishObservers(); foreach (string item in Net.Members.Where((string id) => id != Net.Self && LateJoin.Eligible(id, Plan, Net.Members, departed) && !lateSpectators.Contains(id))) { Send(item, "hello", HostSettings); Send(item, "observer-offer", Plan.Id); } } if (State == Phase.Lobby && observerOffer.Length > 0 && now >= nextObserverRequest && local != null && local.Verify() && GameBridge.InNativeLobby && PhotonNetwork.InRoom && (Object)(object)HUDCanvas.instance != (Object)null && (!((Object)(object)LoadingUI.instance != (Object)null) || !((Component)LoadingUI.instance).gameObject.activeInHierarchy)) { nextObserverRequest = now + 1.0; ToHost("observer-request", new ObserverRequest { Round = observerOffer, Environment = local.Digest }); } } private void ObserverHistoryTick() { if (!Host || !AcceptingSpectators) { observerHistory.Clear(); return; } string[] array = observerHistory.Keys.Where((string id) => !Net.Members.Contains(id) || !lateSpectators.Contains(id)).ToArray(); foreach (string key in array) { observerHistory.Remove(key); } string[] array2 = observerHistory.Keys.ToArray(); for (int num2 = 0; num2 < 2; num2++) { if (array2.Length == 0) { break; } if (Net.PendingPackets >= 256) { break; } string text = array2[historyTurn++ % array2.Length]; Queue<Result> queue = observerHistory[text]; if (queue.Count == 0) { continue; } try { Wire wire = new Wire { Session = Session, Kind = "observer-result", Data = Codec.Write(queue.Peek()) }; if (Net.TrySend(text, wire)) { queue.Dequeue(); } } catch (Exception ex) { NetworkIssue(ex); break; } } } private void AdmitObserver(string sender, ObserverRequest request) { if (Host && AcceptingSpectators && !(request.Round != Plan.Id) && LateJoin.Eligible(sender, Plan, Net.Members, departed) && manifests.TryGetValue(sender, out Manifest value) && value.Verify() && !(value.Digest != Plan.Environment) && !(request.Environment != Plan.Environment)) { if (!lateSpectators.Contains(sender)) { ApplyObservers(lateSpectators.Concat(new string[1] { sender }).ToArray()); PublishObservers(force: true); } Send(sender, "observer-state", new ObserverSnapshot { Plan = Plan, Initial = Initial, Elapsed = Math.Max(0.0, Transport.Now - startLocal), Observers = lateSpectators.Where(((IEnumerable<string>)Net.Members).Contains<string>).ToArray(), Departed = departed.Where(((IEnumerable<string>)Plan.Competitors).Contains<string>).ToArray(), Finished = FinalResults }); observerHistory[sender] = new Queue<Result>(Results.Values); } } private void EnterObservation(ObserverSnapshot snapshot) { if (State != Phase.Lobby || snapshot.Plan.Id != observerOffer || local == null || !local.Verify() || local.Digest != snapshot.Plan.Environment || !GameBridge.InNativeLobby || !PhotonNetwork.InRoom || !LateJoin.ValidSnapshot(snapshot.Plan, snapshot.Initial, snapshot.Elapsed) || !LateJoin.ValidAudience(snapshot.Observers, Net.Members) || !snapshot.Observers.Contains(Net.Self) || snapshot.Departed == null || snapshot.Departed.Any((string id) => !snapshot.Plan.Competitors.Contains(id))) { return; } ResetRoundExchange(); Plan = snapshot.Plan; Initial = snapshot.Initial; HostSettings = Plan.SettingsJson; Multiplayer = true; ApplyObservers(snapshot.Observers); Navigation.Clear(); Results.Clear(); departed.Clear(); transition = null; pendingAction = ResultAction.None; string[] array = snapshot.Departed; foreach (string item in array) { departed.Add(item); } foreach (string item2 in Plan.Competitors.Where((string id) => !Net.Members.Contains(id))) { departed.Add(item2); } resultWait = new ResultWait(Plan.Id, Plan.Competitors); foreach (string item3 in departed) { resultWait.Withdraw(item3); } startLocal = Transport.Now - snapshot.Elapsed; lastElapsed = snapshot.Elapsed; DeathObserved = false; Damage = 0L; Best = ""; plugin.Game.Observe(Plan); startCommitted = true; SetPhase((snapshot.Finished || FinalResults) ? Phase.Finished : Phase.Running, "Joined as a spectator."); foreach (string item4 in Participants.Where((string id) => id != Net.Self)) { Send(item4, "observer-ready", Plan.Id); } } public void ChooseSpectator(bool value) { if (State == Phase.Lobby && Net.LobbyId.Length > 0) { ToHost("choose-observer", value); } } public void AssignRole(string player, int slot) { if (State == Phase.Lobby && (Host || player == Net.Self)) { ToHost("assign-role", new TeamAssignment { Player = player, Slot = slot }); } } public void ChooseTeam(int number) { if (State == Phase.Lobby && number >= 1 && number <= 24) { ToHost("choose-team", number); } } private void SetTeams(RoundPlan plan, RaceOptions options) { if (plan.Multiplayer) { plan.Spectators = LobbySpectators.Where(((IEnumerable<string>)plan.Players).Contains<string>).ToArray(); plan.Teams = Teams.Assign(plan.Competitors, options.TeamCount, options.RandomTeams ? Rules.RandomInt(1, int.MaxValue) : Hash.Seed(Session), teamChoices); plan.SharedRevives = options.SharedRevives; plan.UnderdogPerMember = options.UnderdogPerMember; } } public void SendPeer(string id, string kind, object value) { Send(id, kind, value); } public void ResultPresented() { if (!Observing && State == Phase.Finished && Plan != null && !ownResultShown) { if (resultDisplayFrame < 0) { resultDisplayFrame = Time.frameCount; } else if (Time.frameCount > resultDisplayFrame) { ownResultShown = true; resultWait.Presented(Net.Self, Plan.Id, Transport.Now); SendResultShown(); } } } private void SendResultShown() { if (ownResultShown && !Host && !shownAcknowledged && Plan != null) { Send(Net.Host, "result-shown", Plan.Id); } } private void ResetRoundExchange() { ResetObservers(); startRequest.Cancel(); startCommitted = false; resultAcks.Clear(); nextResultSend = 0.0; resultWait = null; Pages.Clear(); resultDisplayFrame = -1; ownResultShown = false; shownAcknowledged = false; } private void NetworkIssue(Exception ex) { if (!startCommitted) { Fail("Race connection: " + ex.Message, broadcast: false); } else if (Transport.Now >= nextNetworkWarning) { nextNetworkWarning = Transport.Now + 10.0; plugin.LogMessage("Race communication unavailable; local stage continues: " + ex.GetType().Name); } } private void NetworkTick() { try { Net.Tick(Receive); } catch (Exception ex) { NetworkIssue(ex); } } private void SendOwnResult(bool leaving = false) { if (Observing || Plan == null || !Results.TryGetValue(Net.Self, out Result value)) { return; } foreach (string item in Participants.Where((string id) => id != Net.Self && (leaving || !resultAcks.Contains(id)))) { Send(item, leaving ? "leave-result" : "result", value); } } private static string ActionName(ResultAction action) { return action switch { ResultAction.Lobby => "BACK TO LOBBY", ResultAction.Rematch => "REMATCH", _ => "MAIN MENU", }; } public Race(Plugin plugin) { this.plugin = plugin; scanner = new EnvironmentScan(((BaseUnityPlugin)plugin).Config.ConfigFilePath); } private void SetPhase(Phase state, string status) { Plugin.Log.LogInfo((object)("ScoreRace phase: " + State.ToString() + " -> " + state.ToString() + ". " + status)); State = state; Status = status; phaseTime = Transport.Now; } private void Send(string to, string kind, object value) { try { Net.Send(to, new Wire { Session = Session, Kind = kind, Data = Codec.Write(value) }); } catch (Exception ex) { NetworkIssue(ex); } } private void Broadcast(string kind, object value) { foreach (string item in ((startCommitted && Plan != null) ? Participants : Net.Members).Where((string x) => x != Net.Self)) { Send(item, kind, value); } } private void ToHost(string kind, object value) { if (Host) { Receive(Net.Self, new Wire { Session = Session, Kind = kind, Data = Codec.Write(value) }); } else { Send(Net.Host, kind, value); } } public void OpenSession() { if (!GameBridge.CanOpen && !plugin.Game.Active) { Status = "Start from the main menu or lobby."; return; } Net.Refresh(pinned: false); if (!Host) { Status = "The host must press Start in the standard lobby."; return; } ResetLobby(Guid.NewGuid().ToString("N")); Broadcast("hello", HostSettings); Scan(force: false, null); } public void RequestNativeStart(bool solo) { if (!plugin.Ready || plugin.Game.Active || State == Phase.Preparing || State == Phase.Exiting) { return; } Net.Refresh(pinned: false); if (!Host) { Status = "Waiting for the host to start."; return; } if (!HasSession || State == Phase.Error) { OpenSession(); } if (State != Phase.Lobby) { return; } startRequest.Cancel(); if (Net.Members.Length > 150) { Status = "ScoreRace supports up to 150 participants (144 competitors)."; plugin.LogMessage(Status); return; } if (!startRequest.Request(Net.Members.Count((string id) => !LobbySpectators.Contains(id)), Transport.Now)) { Status = "Assign at least one competitor, then press START GAME again."; return; } Status = "Checking all player mods and configurations before starting."; Plugin.Log.LogInfo((object)("Native start requested: " + (solo ? "Singleplayer" : "Lobby Start"))); if (solo && (Object)(object)MenuManager.instance != (Object)null) { MenuManager.instance.PageCloseAll(); } } private void NativeLobbyTick(double now) { if (returningToLobby || State == Phase.Returning) { return; } bool inNativeLobby = GameBridge.InNativeLobby; if (inNativeLobby && !plugin.Game.Active) { wasNativeLobby = true; plugin.Game.CaptureNormalState(); if (!HasSession && Host) { OpenSession(); } } if (wasNativeLobby && !inNativeLobby && !plugin.Game.Active && !startRequest.Pending && State != Phase.Preparing) { wasNativeLobby = false; ClearSession(); plugin.Game.Restore(); } if (State != Phase.Lobby) { return; } if (HasSession && Net.LobbyId != sessionLobby) { startRequest.Cancel(); ClearSession(); return; } if (observerOffer.Length == 0 && now >= nextAutoReady && !Busy && !waitingReady && !ReadySelf && local != null && local.Verify() && HostSettings.Length > 0) { nextAutoReady = now + 1.0; ToggleReady(); } if (startRequest.Expire(now)) { Status = "Setup check incomplete. Match all mods and configurations, then press Start again."; } else if (startRequest.Take(CanStart, now)) { if (inNativeLobby && (Object)(object)SteamManager.instance != (Object)null) { SteamManager.instance.LockLobby(); } StartRound(); if (State == Phase.Preparing && (Object)(object)MenuManager.instance != (Object)null) { MenuMana