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 AfterlifeArcade v0.3.0
plugins/AfterlifeArcade/AfterlifeArcade.dll
Decompiled 5 days 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.Security; using System.Security.Permissions; using System.Text; using AfterlifeArcade.Core; using AfterlifeArcade.Core.Attribution; using AfterlifeArcade.Core.Bets; using AfterlifeArcade.Core.Bingo; using AfterlifeArcade.Core.Client; using AfterlifeArcade.Core.Economy; using AfterlifeArcade.Core.Events; using AfterlifeArcade.Core.Host; using AfterlifeArcade.Core.Lobby; using AfterlifeArcade.Core.Net; using AfterlifeArcade.Core.Qte; using AfterlifeArcade.Core.Side; using AfterlifeArcade.Core.TileFall; using AfterlifeArcade.Events; using AfterlifeArcade.Input; using AfterlifeArcade.Net; using AfterlifeArcade.Patches; using AfterlifeArcade.Session; using AfterlifeArcade.UI; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Lc.Common; using LethalCompanyInputUtils.Api; using LethalNetworkAPI; using LethalNetworkAPI.Utils; using Microsoft.CodeAnalysis; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Utilities; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: IgnoresAccessChecksTo("LethalCompanyInputUtils")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("thatumi")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: AssemblyInformationalVersion("0.3.0+982c64412eca66d42f5f16530caf0c5b3cd438ec")] [assembly: AssemblyProduct("AfterlifeArcade")] [assembly: AssemblyTitle("AfterlifeArcade")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.0.0")] [module: UnverifiableCode] [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 Lc.Common { internal static class FontProvider { private static TMP_FontAsset? _hudFont; internal static TMP_FontAsset? Get() { if ((Object)(object)_hudFont != (Object)null) { return _hudFont; } TMP_FontAsset val = ReflectionGuard.Run("FontProvider.HUD", delegate { HUDManager instance = HUDManager.Instance; return (!((Object)(object)instance != (Object)null) || !((Object)(object)instance.chatText != (Object)null)) ? null : ((TMP_Text)instance.chatText).font; }, null); if ((Object)(object)val != (Object)null) { _hudFont = val; return val; } TMP_FontAsset val2 = ReflectionGuard.Run("FontProvider.Default", () => TMP_Settings.defaultFontAsset, null); if ((Object)(object)val2 != (Object)null) { return val2; } TMP_FontAsset? obj = ReflectionGuard.Run("FontProvider.Any", () => Resources.FindObjectsOfTypeAll<TMP_FontAsset>().FirstOrDefault(), null); if ((Object)(object)obj == (Object)null) { Log.WarnOnce("FontProvider.none", "FontProvider: no TMP font available yet; text will use TMP defaults"); } return obj; } internal static void Invalidate() { _hudFont = null; } } internal enum LogKind { Debug, Info, Warning, Error } internal static class Log { private static readonly object Gate = new object(); private static readonly HashSet<string> OnceKeys = new HashSet<string>(StringComparer.Ordinal); private static Action<LogKind, string>? _sink; internal static bool DebugEnabled { get; set; } internal static void SetSink(Action<LogKind, string>? sink) { _sink = sink; } internal static void Debug(string message) { if (DebugEnabled) { Write(LogKind.Debug, message); } } internal static void Info(string message) { Write(LogKind.Info, message); } internal static void Warn(string message) { Write(LogKind.Warning, message); } internal static void Error(string message) { Write(LogKind.Error, message); } internal static bool WarnOnce(string key, string message) { if (!MarkOnce(key)) { return false; } Write(LogKind.Warning, message); return true; } internal static bool ErrorOnce(string key, string message) { if (!MarkOnce(key)) { return false; } Write(LogKind.Error, message); return true; } internal static bool MarkOnce(string key) { lock (Gate) { return OnceKeys.Add(key); } } internal static void ResetOnce() { lock (Gate) { OnceKeys.Clear(); } } private static void Write(LogKind kind, string message) { Action<LogKind, string> sink = _sink; try { if (sink != null) { sink(kind, message); } else { Console.WriteLine("[" + kind.ToString() + "] " + message); } } catch { } } } internal static class LogBepInEx { internal static void Init(ManualLogSource source) { Log.SetSink(delegate(LogKind kind, string message) { switch (kind) { case LogKind.Debug: source.LogDebug((object)message); break; case LogKind.Info: source.LogInfo((object)message); break; case LogKind.Warning: source.LogWarning((object)message); break; default: source.LogError((object)message); break; } }); } } internal static class ReflectionGuard { internal const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags Declared = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly object Gate = new object(); private static readonly Dictionary<string, string> DisabledReasons = new Dictionary<string, string>(StringComparer.Ordinal); private static readonly Dictionary<string, int> FailureCounts = new Dictionary<string, int>(StringComparer.Ordinal); private static readonly Dictionary<string, Type?> TypeCache = new Dictionary<string, Type>(StringComparer.Ordinal); internal static int MaxFailuresBeforeDisable { get; set; } = 25; internal static IReadOnlyDictionary<string, string> DisabledFeatures { get { lock (Gate) { return new Dictionary<string, string>(DisabledReasons, StringComparer.Ordinal); } } } internal static event Action<string, string>? FeatureDisabled; internal static bool IsEnabled(string feature) { lock (Gate) { return !DisabledReasons.ContainsKey(feature); } } internal static void Disable(string feature, string reason) { lock (Gate) { if (DisabledReasons.ContainsKey(feature)) { return; } DisabledReasons[feature] = reason; } Log.Warn("[" + feature + "] disabled: " + reason); try { ReflectionGuard.FeatureDisabled?.Invoke(feature, reason); } catch { } } internal static void Reset() { lock (Gate) { DisabledReasons.Clear(); FailureCounts.Clear(); TypeCache.Clear(); } } internal static Type? FindType(string fullName, string feature, string? assemblyName = null) { string key = (assemblyName ?? "*") + "|" + fullName; lock (Gate) { if (TypeCache.TryGetValue(key, out Type value) && value != null) { return value; } } Type type = null; try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (assemblyName == null || string.Equals(assembly.GetName().Name, assemblyName, StringComparison.Ordinal)) { type = assembly.GetType(fullName, throwOnError: false); if (type != null) { break; } } } } catch (Exception ex) { Disable(feature, "type lookup " + fullName + " threw " + ex.GetType().Name + ": " + ex.Message); return null; } if (type != null) { lock (Gate) { TypeCache[key] = type; } } else { Disable(feature, "type " + fullName + ((assemblyName != null) ? (" (in " + assemblyName + ")") : "") + " not found"); } return type; } internal static FieldInfo? Field(Type? type, string name, string feature) { if (type == null) { Disable(feature, "field " + name + ": owner type is null"); return null; } Type type2 = type; while (type2 != null) { FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field; } type2 = type2.BaseType; } Disable(feature, "field " + type.FullName + "." + name + " not found"); return null; } internal static MethodInfo? Method(Type? type, string name, Type[]? parameterTypes, string feature) { if (type == null) { Disable(feature, "method " + name + ": owner type is null"); return null; } Type type2 = type; while (type2 != null) { List<MethodInfo> list = (from m in type2.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where m.Name == name select m).ToList(); if (list.Count != 0) { if (parameterTypes == null) { if (list.Count == 1) { return list[0]; } Disable(feature, "method " + type2.FullName + "." + name + " is ambiguous (" + list.Count + " overloads); pass parameter types"); return null; } foreach (MethodInfo item in list) { if (ParametersMatch(item.GetParameters(), parameterTypes)) { return item; } } } type2 = type2.BaseType; } Disable(feature, "method " + type.FullName + "." + name + Signature(parameterTypes) + " not found"); return null; } internal static PropertyInfo? Property(Type? type, string name, string feature) { if (type == null) { Disable(feature, "property " + name + ": owner type is null"); return null; } Type type2 = type; while (type2 != null) { PropertyInfo property = type2.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property; } type2 = type2.BaseType; } Disable(feature, "property " + type.FullName + "." + name + " not found"); return null; } internal static bool TryGet<T>(FieldInfo? field, object? instance, out T value, string feature) { value = default(T); if (field == null || !IsEnabled(feature)) { return false; } try { object value2 = field.GetValue(field.IsStatic ? null : instance); if (value2 is T val) { value = val; return true; } if (value2 == null && default(T) == null) { return true; } Log.WarnOnce(feature + "|type|" + field.Name, "[" + feature + "] field " + field.Name + " is " + (value2?.GetType().FullName ?? "null") + ", expected " + typeof(T).FullName); return false; } catch (Exception e) { CountFailure(feature, "get " + field.Name, e); return false; } } internal static bool TryGet<T>(PropertyInfo? property, object? instance, out T value, string feature) { value = default(T); MethodInfo methodInfo = property?.GetGetMethod(nonPublic: true); if (property == null || methodInfo == null || !IsEnabled(feature)) { return false; } try { object value2 = property.GetValue(methodInfo.IsStatic ? null : instance, null); if (value2 is T val) { value = val; return true; } if (value2 == null && default(T) == null) { return true; } Log.WarnOnce(feature + "|type|" + property.Name, "[" + feature + "] property " + property.Name + " is " + (value2?.GetType().FullName ?? "null") + ", expected " + typeof(T).FullName); return false; } catch (Exception e) { CountFailure(feature, "get " + property.Name, e); return false; } } internal static bool TrySet(FieldInfo? field, object? instance, object? value, string feature) { if (field == null || !IsEnabled(feature)) { return false; } try { field.SetValue(field.IsStatic ? null : instance, value); return true; } catch (Exception e) { CountFailure(feature, "set " + field.Name, e); return false; } } internal static bool TryInvoke(MethodInfo? method, object? instance, object?[]? args, out object? result, string feature) { result = null; if (method == null || !IsEnabled(feature)) { return false; } try { result = method.Invoke(method.IsStatic ? null : instance, args); return true; } catch (TargetInvocationException ex) when (ex.InnerException != null) { CountFailure(feature, "invoke " + method.Name, ex.InnerException); return false; } catch (Exception e) { CountFailure(feature, "invoke " + method.Name, e); return false; } } internal static void Run(string key, Action body) { if (!IsEnabled(key)) { return; } try { body(); } catch (Exception e) { CountFailure(key, "threw", e); } } internal static T Run<T>(string key, Func<T> body, T fallback) { if (!IsEnabled(key)) { return fallback; } try { return body(); } catch (Exception e) { CountFailure(key, "threw", e); return fallback; } } internal static bool Prefix(string key, Func<bool> body) { return Run(key, body, fallback: true); } internal static int FailureCount(string key) { lock (Gate) { int value; return FailureCounts.TryGetValue(key, out value) ? value : 0; } } private static void CountFailure(string key, string what, Exception e) { int value; lock (Gate) { FailureCounts.TryGetValue(key, out value); value++; FailureCounts[key] = value; } if (value == 1) { Log.Error("[" + key + "] " + what + ": " + e); } if (value >= MaxFailuresBeforeDisable) { Disable(key, value + " exceptions (first logged above)"); } } private static bool ParametersMatch(ParameterInfo[] actual, Type[] expected) { if (actual.Length != expected.Length) { return false; } for (int i = 0; i < actual.Length; i++) { if (actual[i].ParameterType != expected[i]) { return false; } } return true; } private static string Signature(Type[]? parameterTypes) { if (parameterTypes != null) { return "(" + string.Join(", ", parameterTypes.Select((Type t) => t.Name)) + ")"; } return ""; } } internal static class TexGen { private delegate float CoverageFn(float x, float y); private static readonly Dictionary<string, Sprite> Cache = new Dictionary<string, Sprite>(); internal static Sprite RoundedRect(int radius = 12, int size = 0) { if (size <= 0) { size = radius * 2 + 4; } string key = "rr|" + radius + "|" + size; return Get(key, delegate { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) Texture2D obj = NewTexture(size, size, key); Paint(obj, (float x, float y) => TexGenMath.FillCoverage(TexGenMath.RoundedRectDistance(x, y, size, size, radius))); return Sprite.Create(obj, new Rect(0f, 0f, (float)size, (float)size), new Vector2(0.5f, 0.5f), 100f, 0u, (SpriteMeshType)0, new Vector4((float)(radius + 1), (float)(radius + 1), (float)(radius + 1), (float)(radius + 1))); }); } internal static Sprite Outline(int radius = 12, int thickness = 2, int size = 0) { if (size <= 0) { size = radius * 2 + 4; } string key = "ol|" + radius + "|" + thickness + "|" + size; return Get(key, delegate { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) Texture2D obj = NewTexture(size, size, key); Paint(obj, (float x, float y) => TexGenMath.StrokeCoverage(TexGenMath.RoundedRectDistance(x, y, size, size, radius), thickness)); int num = Mathf.Max(radius, thickness) + 1; return Sprite.Create(obj, new Rect(0f, 0f, (float)size, (float)size), new Vector2(0.5f, 0.5f), 100f, 0u, (SpriteMeshType)0, new Vector4((float)num, (float)num, (float)num, (float)num)); }); } internal static Sprite Circle(int diameter = 64, int thickness = 0) { string key = "ci|" + diameter + "|" + thickness; return Get(key, delegate { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) Texture2D obj = NewTexture(diameter, diameter, key); float r = (float)diameter * 0.5f; Paint(obj, delegate(float x, float y) { float distance = TexGenMath.CircleDistance(x, y, r, r, r - 0.5f); return (thickness <= 0) ? TexGenMath.FillCoverage(distance) : TexGenMath.StrokeCoverage(distance, thickness); }); return Sprite.Create(obj, new Rect(0f, 0f, (float)diameter, (float)diameter), new Vector2(0.5f, 0.5f), 100f); }); } internal static Sprite White() { return Get("white", delegate { //IL_000f: 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_0044: Unknown result type (might be due to invalid IL or missing references) Texture2D obj = NewTexture(1, 1, "white"); obj.SetPixel(0, 0, Color.white); obj.Apply(false, false); return Sprite.Create(obj, new Rect(0f, 0f, 1f, 1f), new Vector2(0.5f, 0.5f), 100f); }); } private static Sprite Get(string key, Func<Sprite> create) { if (Cache.TryGetValue(key, out Sprite value) && (Object)(object)value != (Object)null) { return value; } value = create(); ((Object)value).name = "LcTexGen_" + key; ((Object)value).hideFlags = (HideFlags)52; Cache[key] = value; return value; } private static Texture2D NewTexture(int w, int h, string key) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown return new Texture2D(w, h, (TextureFormat)4, false) { name = "LcTexGen_" + key, wrapMode = (TextureWrapMode)1, filterMode = (FilterMode)1, hideFlags = (HideFlags)52 }; } private static void Paint(Texture2D tex, CoverageFn coverage) { //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) int width = ((Texture)tex).width; int height = ((Texture)tex).height; Color32[] array = (Color32[])(object)new Color32[width * height]; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { float num = coverage((float)j + 0.5f, (float)i + 0.5f); array[i * width + j] = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)Mathf.RoundToInt(num * 255f)); } } tex.SetPixels32(array); tex.Apply(false, false); } } internal static class TexGenMath { internal static float RoundedRectDistance(float px, float py, float w, float h, float r) { r = Math.Max(0f, Math.Min(r, Math.Min(w, h) * 0.5f)); float num = w * 0.5f; float num2 = h * 0.5f; float val = Math.Abs(px - num) - (num - r); float num3 = Math.Abs(py - num2) - (num2 - r); float num4 = Math.Max(val, 0f); float num5 = Math.Max(num3, 0f); float num6 = (float)Math.Sqrt(num4 * num4 + num5 * num5); float num7 = Math.Min(Math.Max(val, num3), 0f); return num6 + num7 - r; } internal static float CircleDistance(float px, float py, float cx, float cy, float r) { float num = px - cx; float num2 = py - cy; return (float)Math.Sqrt(num * num + num2 * num2) - r; } internal static float FillCoverage(float distance) { return Clamp01(0.5f - distance); } internal static float StrokeCoverage(float distance, float thickness) { float num = FillCoverage(distance); float num2 = FillCoverage(distance + thickness); return Clamp01(num - num2); } internal static float Clamp01(float v) { if (!(v < 0f)) { if (!(v > 1f)) { return v; } return 1f; } return 0f; } } internal static class UiFactory { internal static class Theme { internal static Color PanelBg = new Color(0.05f, 0.05f, 0.06f, 0.92f); internal static Color PanelBorder = new Color(1f, 0.45f, 0.1f, 0.9f); internal static Color Text = new Color(0.98f, 0.62f, 0.25f, 1f); internal static Color TextDim = new Color(0.75f, 0.6f, 0.45f, 1f); internal static Color ButtonBg = new Color(0.16f, 0.09f, 0.05f, 0.95f); internal static Color ButtonHighlight = new Color(0.45f, 0.22f, 0.07f, 1f); internal static Color ButtonPressed = new Color(0.7f, 0.33f, 0.08f, 1f); internal static Color ButtonDisabled = new Color(0.12f, 0.12f, 0.12f, 0.7f); internal static Color Danger = new Color(0.9f, 0.18f, 0.15f, 1f); internal static Color BackdropColor = new Color(0f, 0f, 0f, 0.72f); internal static int CornerRadius = 10; internal static float FontSize = 26f; } internal static readonly Vector2 ReferenceResolution = new Vector2(1920f, 1080f); internal static Canvas Canvas(string name, int sortingOrder, bool dontDestroyOnLoad = false) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown //IL_0053: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) }); val.layer = 5; if (dontDestroyOnLoad) { Object.DontDestroyOnLoad((Object)(object)val); } Canvas obj = val.AddComponent<Canvas>(); obj.renderMode = (RenderMode)0; obj.sortingOrder = sortingOrder; obj.pixelPerfect = false; CanvasScaler obj2 = val.AddComponent<CanvasScaler>(); obj2.uiScaleMode = (ScaleMode)1; obj2.referenceResolution = ReferenceResolution; obj2.screenMatchMode = (ScreenMatchMode)0; obj2.matchWidthOrHeight = 0.5f; val.AddComponent<GraphicRaycaster>(); return obj; } internal static RectTransform Rect(Transform parent, string name) { //IL_0014: 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_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown RectTransform val = (RectTransform)new GameObject(name, new Type[1] { typeof(RectTransform) }) { layer = 5 }.transform; ((Transform)val).SetParent(parent, false); return val; } internal static RectTransform Fill(RectTransform rt, float padding = 0f) { //IL_0001: 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_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.pivot = new Vector2(0.5f, 0.5f); rt.offsetMin = new Vector2(padding, padding); rt.offsetMax = new Vector2(0f - padding, 0f - padding); return rt; } internal static RectTransform Place(RectTransform rt, Vector2 anchor, Vector2 offset, Vector2 size, Vector2? pivot = null) { //IL_0001: 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_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) rt.anchorMin = anchor; rt.anchorMax = anchor; rt.pivot = pivot ?? anchor; rt.anchoredPosition = offset; rt.sizeDelta = size; return rt; } internal static RectTransform PlaceTL(RectTransform rt, float cx, float cy, float w, float h) { //IL_000b: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) rt.anchorMin = new Vector2(0f, 1f); rt.anchorMax = new Vector2(0f, 1f); rt.pivot = new Vector2(0.5f, 0.5f); rt.anchoredPosition = new Vector2(cx, 0f - cy); rt.sizeDelta = new Vector2(w, h); return rt; } internal static LayoutElement Size(Component c, float width = -1f, float height = -1f, float flexibleWidth = -1f) { LayoutElement val = c.GetComponent<LayoutElement>(); if ((Object)(object)val == (Object)null) { val = c.gameObject.AddComponent<LayoutElement>(); } val.preferredWidth = width; val.preferredHeight = height; val.minHeight = height; val.flexibleWidth = flexibleWidth; return val; } internal static VerticalLayoutGroup Vertical(Component c, float spacing = 8f, int padding = 12) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown VerticalLayoutGroup obj = c.gameObject.AddComponent<VerticalLayoutGroup>(); ((HorizontalOrVerticalLayoutGroup)obj).spacing = spacing; ((LayoutGroup)obj).padding = new RectOffset(padding, padding, padding, padding); ((LayoutGroup)obj).childAlignment = (TextAnchor)0; ((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandHeight = false; return obj; } internal static HorizontalLayoutGroup Horizontal(Component c, float spacing = 8f, int padding = 0) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown HorizontalLayoutGroup obj = c.gameObject.AddComponent<HorizontalLayoutGroup>(); ((HorizontalOrVerticalLayoutGroup)obj).spacing = spacing; ((LayoutGroup)obj).padding = new RectOffset(padding, padding, padding, padding); ((LayoutGroup)obj).childAlignment = (TextAnchor)3; ((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandWidth = false; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandHeight = false; return obj; } internal static Image Panel(Transform parent, string name, Color? color = null, bool border = true) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0041: 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) RectTransform val = Fill(Rect(parent, name)); Image obj = ((Component)val).gameObject.AddComponent<Image>(); obj.sprite = TexGen.RoundedRect(Theme.CornerRadius); obj.type = (Type)1; ((Graphic)obj).color = (Color)(((??)color) ?? Theme.PanelBg); ((Graphic)obj).raycastTarget = true; if (border) { Image obj2 = ((Component)Fill(Rect((Transform)(object)val, "Border"))).gameObject.AddComponent<Image>(); obj2.sprite = TexGen.Outline(Theme.CornerRadius); obj2.type = (Type)1; ((Graphic)obj2).color = Theme.PanelBorder; ((Graphic)obj2).raycastTarget = false; } return obj; } internal static TextMeshProUGUI Text(Transform parent, string text, float size = 0f, TextAlignmentOptions align = (TextAlignmentOptions)513, Color? color = null, string name = "Text") { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI val = ((Component)Rect(parent, name)).gameObject.AddComponent<TextMeshProUGUI>(); TMP_FontAsset val2 = FontProvider.Get(); if ((Object)(object)val2 != (Object)null) { ((TMP_Text)val).font = val2; } ((TMP_Text)val).text = text; ((TMP_Text)val).fontSize = ((size > 0f) ? size : Theme.FontSize); ((TMP_Text)val).alignment = align; ((Graphic)val).color = (Color)(((??)color) ?? Theme.Text); ((TMP_Text)val).enableWordWrapping = true; ((TMP_Text)val).overflowMode = (TextOverflowModes)1; ((Graphic)val).raycastTarget = false; return val; } internal static Button Button(Transform parent, string label, Action onClick, Vector2? size = null, string? name = null) { //IL_0036: 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_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) RectTransform obj = Rect(parent, name ?? ("Button_" + label)); obj.sizeDelta = (Vector2)(((??)size) ?? new Vector2(260f, 52f)); Image val = ((Component)obj).gameObject.AddComponent<Image>(); val.sprite = TexGen.RoundedRect(Theme.CornerRadius); val.type = (Type)1; ((Graphic)val).color = Color.white; Button val2 = ((Component)obj).gameObject.AddComponent<Button>(); ((Selectable)val2).targetGraphic = (Graphic)(object)val; ((Selectable)val2).colors = Colors(); ((UnityEvent)val2.onClick).AddListener(Guarded("Button " + label, onClick)); Fill(((TMP_Text)Text((Transform)(object)obj, label, 0f, (TextAlignmentOptions)514, null, "Label")).rectTransform, 6f); if (size.HasValue) { Size((Component)(object)val2, size.Value.x, size.Value.y); } return val2; } internal static Button CycleButton(Transform parent, Func<string> label, Action onClick, Vector2? size = null, string? name = null) { TextMeshProUGUI text = null; Button val = Button(parent, label(), delegate { onClick(); if ((Object)(object)text != (Object)null) { ((TMP_Text)text).text = label(); } }, size, name); text = ((Component)val).GetComponentInChildren<TextMeshProUGUI>(true); return val; } internal static void SetLabel(Button button, string label) { TextMeshProUGUI componentInChildren = ((Component)button).GetComponentInChildren<TextMeshProUGUI>(true); if ((Object)(object)componentInChildren != (Object)null) { ((TMP_Text)componentInChildren).text = label; } } internal static Image Picture(Transform parent, string name, Sprite? sprite, Color color, bool raycast = false) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) Image obj = ((Component)Rect(parent, name)).gameObject.AddComponent<Image>(); obj.sprite = (Sprite)(((Object)(object)sprite != (Object)null) ? ((object)sprite) : ((object)TexGen.White())); ((Graphic)obj).color = color; ((Graphic)obj).raycastTarget = raycast; obj.preserveAspect = (Object)(object)sprite != (Object)null; return obj; } internal static Image Line(RectTransform parent, Vector2 from, Vector2 to, float thickness, Color color) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) Image obj = Picture((Transform)(object)parent, "Line", null, color); UiMath.Line(from.x, from.y, to.x, to.y, out var centreX, out var centreY, out var length, out var angleDeg); PlaceTL(((Graphic)obj).rectTransform, centreX, centreY, length, thickness); ((Transform)((Graphic)obj).rectTransform).localRotation = Quaternion.Euler(0f, 0f, angleDeg); return obj; } internal static RectTransform Backdrop(Transform parent, string name = "Backdrop", Color? color = null) { //IL_003c: 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) RectTransform obj = Fill(Rect(parent, name)); Image obj2 = ((Component)obj).gameObject.AddComponent<Image>(); obj2.sprite = TexGen.White(); ((Graphic)obj2).color = (Color)(((??)color) ?? Theme.BackdropColor); ((Graphic)obj2).raycastTarget = true; return obj; } internal static Toggle Toggle(Transform parent, string label, bool value, Action<bool> onChanged, float height = 44f) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_005b: 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_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Unknown result type (might be due to invalid IL or missing references) RectTransform obj = Rect(parent, "Toggle_" + label); obj.sizeDelta = new Vector2(400f, height); Size((Component)(object)obj, -1f, height, 1f); Image val = ((Component)obj).gameObject.AddComponent<Image>(); val.sprite = TexGen.RoundedRect(Theme.CornerRadius); val.type = (Type)1; ((Graphic)val).color = Color.white; RectTransform obj2 = Place(Rect((Transform)(object)obj, "Box"), new Vector2(0f, 0.5f), new Vector2(8f, 0f), new Vector2(height - 12f, height - 12f)); Image obj3 = ((Component)obj2).gameObject.AddComponent<Image>(); obj3.sprite = TexGen.Outline(6); obj3.type = (Type)1; ((Graphic)obj3).color = Theme.Text; ((Graphic)obj3).raycastTarget = false; Image val2 = ((Component)Fill(Rect((Transform)(object)obj2, "Check"), 6f)).gameObject.AddComponent<Image>(); val2.sprite = TexGen.RoundedRect(4); val2.type = (Type)1; ((Graphic)val2).color = Theme.Text; ((Graphic)val2).raycastTarget = false; TextMeshProUGUI obj4 = Text((Transform)(object)obj, label, 0f, (TextAlignmentOptions)513, null, "Label"); ((TMP_Text)obj4).rectTransform.anchorMin = new Vector2(0f, 0f); ((TMP_Text)obj4).rectTransform.anchorMax = new Vector2(1f, 1f); ((TMP_Text)obj4).rectTransform.offsetMin = new Vector2(height + 4f, 0f); ((TMP_Text)obj4).rectTransform.offsetMax = new Vector2(-8f, 0f); Toggle obj5 = ((Component)obj).gameObject.AddComponent<Toggle>(); ((Selectable)obj5).targetGraphic = (Graphic)(object)val; obj5.graphic = (Graphic)(object)val2; ((Selectable)obj5).colors = Colors(transparentNormal: true); obj5.isOn = value; UnityAction<bool> val3 = Guarded("Toggle " + label, onChanged); ((UnityEvent<bool>)(object)obj5.onValueChanged).AddListener(val3); return obj5; } internal static ScrollRect ScrollList(Transform parent, string name, out RectTransform content, float spacing = 6f) { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: 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_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: 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) RectTransform obj = Fill(Rect(parent, name)); ScrollRect val = ((Component)obj).gameObject.AddComponent<ScrollRect>(); val.horizontal = false; val.vertical = true; val.movementType = (MovementType)2; val.scrollSensitivity = 30f; RectTransform val2 = Fill(Rect((Transform)(object)obj, "Viewport")); ((Component)val2).gameObject.AddComponent<RectMask2D>(); ((Graphic)((Component)val2).gameObject.AddComponent<Image>()).color = new Color(0f, 0f, 0f, 0f); content = Rect((Transform)(object)val2, "Content"); content.anchorMin = new Vector2(0f, 1f); content.anchorMax = new Vector2(1f, 1f); content.pivot = new Vector2(0.5f, 1f); content.anchoredPosition = Vector2.zero; content.sizeDelta = Vector2.zero; ((LayoutGroup)Vertical((Component)(object)content, spacing, 4)).childAlignment = (TextAnchor)1; ContentSizeFitter obj2 = ((Component)content).gameObject.AddComponent<ContentSizeFitter>(); obj2.verticalFit = (FitMode)2; obj2.horizontalFit = (FitMode)0; val.viewport = val2; val.content = content; return val; } internal static void ScrollIntoView(ScrollRect scroll, RectTransform item) { ReflectionGuard.Run("UiFactory.ScrollIntoView", delegate { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_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) //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_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00be: 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_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) RectTransform content = scroll.content; RectTransform val = (RectTransform)(((Object)(object)scroll.viewport != (Object)null) ? ((object)scroll.viewport) : ((object)(RectTransform)((Component)scroll).transform)); if (!((Object)(object)content == (Object)null)) { Canvas.ForceUpdateCanvases(); Rect rect = content.rect; float height = ((Rect)(ref rect)).height; rect = val.rect; float height2 = ((Rect)(ref rect)).height; if (!(height <= height2)) { float y = ((Transform)content).InverseTransformPoint(((Transform)item).position).y; rect = item.rect; float num = 0f - (y + ((Rect)(ref rect)).height * (1f - item.pivot.y)); rect = item.rect; float num2 = num + ((Rect)(ref rect)).height; float num3 = content.anchoredPosition.y; if (num < num3) { num3 = num; } else if (num2 > num3 + height2) { num3 = num2 - height2; } num3 = Mathf.Clamp(num3, 0f, height - height2); content.anchoredPosition = new Vector2(content.anchoredPosition.x, num3); } } }); } internal static void LinkVertical(IList<Selectable> items, bool wrap = true, Selectable? left = null, Selectable? right = null) { Link(items, wrap, vertical: true, left, right); } internal static void LinkHorizontal(IList<Selectable> items, bool wrap = false, Selectable? up = null, Selectable? down = null) { Link(items, wrap, vertical: false, up, down); } internal static void Select(Selectable? target) { ReflectionGuard.Run("UiFactory.Select", delegate { EventSystem current = EventSystem.current; if (!((Object)(object)current == (Object)null) && !((Object)(object)target == (Object)null) && ((Component)target).gameObject.activeInHierarchy) { current.SetSelectedGameObject((GameObject)null); current.SetSelectedGameObject(((Component)target).gameObject); } }); } internal static void Destroy(Component? root) { if ((Object)(object)root != (Object)null) { Object.Destroy((Object)(object)root.gameObject); } } private static void Link(IList<Selectable> items, bool wrap, bool vertical, Selectable? sideA, Selectable? sideB) { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) List<Selectable> list = new List<Selectable>(); foreach (Selectable item in items) { if ((Object)(object)item != (Object)null && ((Component)item).gameObject.activeInHierarchy && item.IsInteractable()) { list.Add(item); } } for (int i = 0; i < list.Count; i++) { Selectable val = ((i > 0) ? list[i - 1] : ((wrap && list.Count > 1) ? list[list.Count - 1] : null)); Selectable val2 = ((i < list.Count - 1) ? list[i + 1] : ((wrap && list.Count > 1) ? list[0] : null)); Navigation val3 = default(Navigation); ((Navigation)(ref val3)).mode = (Mode)4; Navigation navigation = val3; if (vertical) { ((Navigation)(ref navigation)).selectOnUp = val; ((Navigation)(ref navigation)).selectOnDown = val2; ((Navigation)(ref navigation)).selectOnLeft = sideA; ((Navigation)(ref navigation)).selectOnRight = sideB; } else { ((Navigation)(ref navigation)).selectOnLeft = val; ((Navigation)(ref navigation)).selectOnRight = val2; ((Navigation)(ref navigation)).selectOnUp = sideA; ((Navigation)(ref navigation)).selectOnDown = sideB; } list[i].navigation = navigation; } } private static ColorBlock Colors(bool transparentNormal = false) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) ColorBlock defaultColorBlock = ColorBlock.defaultColorBlock; ((ColorBlock)(ref defaultColorBlock)).normalColor = (Color)(transparentNormal ? new Color(0f, 0f, 0f, 0f) : Theme.ButtonBg); ((ColorBlock)(ref defaultColorBlock)).highlightedColor = Theme.ButtonHighlight; ((ColorBlock)(ref defaultColorBlock)).selectedColor = Theme.ButtonHighlight; ((ColorBlock)(ref defaultColorBlock)).pressedColor = Theme.ButtonPressed; ((ColorBlock)(ref defaultColorBlock)).disabledColor = Theme.ButtonDisabled; ((ColorBlock)(ref defaultColorBlock)).colorMultiplier = 1f; ((ColorBlock)(ref defaultColorBlock)).fadeDuration = 0.08f; return defaultColorBlock; } private static UnityAction Guarded(string key, Action action) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown return (UnityAction)delegate { ReflectionGuard.Run(key, action); }; } private static UnityAction<T> Guarded<T>(string key, Action<T> action) { return delegate(T v) { ReflectionGuard.Run(key, delegate { action(v); }); }; } } internal static class UiMath { internal static void Line(float fromX, float fromY, float toX, float toY, out float centreX, out float centreY, out float length, out float angleDeg) { float num = toX - fromX; float num2 = 0f - (toY - fromY); centreX = (fromX + toX) * 0.5f; centreY = (fromY + toY) * 0.5f; length = (float)Math.Sqrt(num * num + num2 * num2); angleDeg = ((length > 0f) ? ((float)(Math.Atan2(num2, num) * (180.0 / Math.PI))) : 0f); } } } namespace AfterlifeArcade { internal enum FallbackPadToggle { None, Select, LeftStickPress, RightStickPress, ButtonNorth } internal sealed class ArcadeConfig { internal ConfigEntry<bool> Enabled { get; } internal ConfigEntry<bool> AutoOpen { get; } internal ConfigEntry<float> AutoOpenDelay { get; } internal ConfigEntry<int> MinPlayers { get; } internal ConfigEntry<bool> EnableTileFall { get; } internal ConfigEntry<bool> EnableQteDuel { get; } internal ConfigEntry<int> VoteSeconds { get; } internal ConfigEntry<int> TileFallMaxSeconds { get; } internal ConfigEntry<float> OverlayOpacity { get; } internal ConfigEntry<bool> DimSpectateView { get; } internal ConfigEntry<float> SoundVolume { get; } internal ConfigEntry<FallbackPadToggle> PadToggle { get; } internal ConfigEntry<bool> MiniHud { get; } internal ConfigEntry<bool> BetsEnabled { get; } internal ConfigEntry<string> Stakes { get; } internal ConfigEntry<float> LateWindowSeconds { get; } internal ConfigEntry<float> TogetherWindowSeconds { get; } internal ConfigEntry<int> PropMarkets { get; } internal ConfigEntry<bool> DisableIfEnhancedSpectator { get; } internal ConfigEntry<bool> LineForTheLiving { get; } internal ConfigEntry<int> EctoStart { get; } internal ConfigEntry<int> EctoFloor { get; } internal ConfigEntry<int> EctoTopUp { get; } internal ConfigEntry<bool> BingoEnabled { get; } internal ConfigEntry<int> CardSize { get; } internal ConfigEntry<CountMode> BingoCountMode { get; } internal ConfigEntry<bool> CompanyPool { get; } internal ConfigEntry<bool> UseCoroner { get; } internal ConfigEntry<bool> DeathRecap { get; } internal ConfigEntry<int> PracticeBestMs { get; } internal ConfigEntry<bool> DebugLog { get; } internal ArcadeConfig(ConfigFile file) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Expected O, but got Unknown //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Expected O, but got Unknown //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Expected O, but got Unknown //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Expected O, but got Unknown //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Expected O, but got Unknown //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Expected O, but got Unknown //IL_026a: Unknown result type (might be due to invalid IL or missing references) //IL_0274: Expected O, but got Unknown //IL_02a3: Unknown result type (might be due to invalid IL or missing references) //IL_02ad: Expected O, but got Unknown //IL_02d0: Unknown result type (might be due to invalid IL or missing references) //IL_02da: Expected O, but got Unknown //IL_033a: Unknown result type (might be due to invalid IL or missing references) //IL_0344: Expected O, but got Unknown //IL_036c: Unknown result type (might be due to invalid IL or missing references) //IL_0376: Expected O, but got Unknown //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03a8: Expected O, but got Unknown //IL_03f3: Unknown result type (might be due to invalid IL or missing references) //IL_03fd: Expected O, but got Unknown Enabled = file.Bind<bool>("General", "Enabled", true, "Master switch. When off, the mod does nothing (you are simply absent from the arcade)."); AutoOpen = file.Bind<bool>("General", "AutoOpenOnDeath", true, "Open the arcade overlay automatically after you die."); AutoOpenDelay = file.Bind<float>("General", "AutoOpenDelaySeconds", 3f, new ConfigDescription("Seconds after death before the overlay opens (so you can watch your death first).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); MinPlayers = file.Bind<int>("Host", "MinPlayers", 2, new ConfigDescription("Host only: dead players with the overlay open needed to start a match.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(2, 8), Array.Empty<object>())); EnableTileFall = file.Bind<bool>("Host", "EnableTileFall", true, "Host only: offer Tile Fall."); EnableQteDuel = file.Bind<bool>("Host", "EnableReactionDuel", true, "Host only: offer the Reaction Duel (QTE)."); VoteSeconds = file.Bind<int>("Host", "VoteSeconds", 10, new ConfigDescription("Host only: length of the game vote.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(3, 60), Array.Empty<object>())); TileFallMaxSeconds = file.Bind<int>("Host", "TileFallMaxSeconds", 90, new ConfigDescription("Host only: Tile Fall time limit (survivors share the win).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 180), Array.Empty<object>())); OverlayOpacity = file.Bind<float>("UI", "OverlayOpacity", 0.92f, new ConfigDescription("Opacity of the arcade panel.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.3f, 1f), Array.Empty<object>())); DimSpectateView = file.Bind<bool>("UI", "DimSpectateView", false, "Darken the spectate camera behind the overlay."); SoundVolume = file.Bind<float>("UI", "SoundVolume", 0.6f, new ConfigDescription("Volume of the arcade's beeps.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); PadToggle = file.Bind<FallbackPadToggle>("Input", "PadToggleWithoutInputUtils", FallbackPadToggle.None, "Gamepad button that toggles the overlay when InputUtils is NOT installed. With InputUtils, bind 'Toggle Afterlife Arcade' in its keybind menu instead."); MiniHud = file.Bind<bool>("UI", "MiniHud", true, "While dead: show the small bingo card and bet ticker at the right edge, even with the overlay closed."); BetsEnabled = file.Bind<bool>("Bets", "Enabled", true, "Host: run Spectator Betting (dead players bet cosmetic Ecto on the round). Takes effect at the next session; a change of this or Bingo.Enabled from both off to on needs a game restart."); Stakes = file.Bind<string>("Bets", "Stakes", "10,25,50", "Host: the allowed stakes (up to 4, comma separated, 1-1000)."); LateWindowSeconds = file.Bind<float>("Bets", "LateWindowSeconds", 5f, new ConfigDescription("Host: a bet placed less than this before its outcome became visible is refunded.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); TogetherWindowSeconds = file.Bind<float>("Bets", "TogetherWindowSeconds", 1.5f, new ConfigDescription("Host: 'Next to die' counts deaths this close to the first one as one answer (any of them wins).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); PropMarkets = file.Bind<int>("Bets", "PropMarkets", 1, new ConfigDescription("Host: yes/no markets on round events per round ('Will a mine go off?').", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 2), Array.Empty<object>())); DisableIfEnhancedSpectator = file.Bind<bool>("Bets", "DisableIfEnhancedSpectator", true, "Turn Spectator Betting off when EnhancedSpectator is loaded (it has its own betting UI). On the host this switches the markets off; on a client it hides the bets tab."); LineForTheLiving = file.Bind<bool>("Bets", "LineForTheLiving", true, "At the end of a round you survived, show one line about what the dead bet on you."); EctoStart = file.Bind<int>("Ecto", "StartBalance", 100, new ConfigDescription("Host: Ecto a player starts with.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10000), Array.Empty<object>())); EctoFloor = file.Bind<int>("Ecto", "Floor", 10, new ConfigDescription("Host: at round start, anyone below this is topped up to TopUp.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10000), Array.Empty<object>())); EctoTopUp = file.Bind<int>("Ecto", "TopUp", 20, new ConfigDescription("Host: the top-up balance.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10000), Array.Empty<object>())); BingoEnabled = file.Bind<bool>("Bingo", "Enabled", true, "Host: deal Ghost Bingo cards to dead players (marked by what happens in the round)."); CardSize = file.Bind<int>("Bingo", "CardSize", 4, new ConfigDescription("Host: 4 = 4x4 (10 lines), 5 = 5x5 with a free centre (12 lines). Company days use 3x3.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[2] { 4, 5 }), Array.Empty<object>())); BingoCountMode = file.Bind<CountMode>("Bingo", "CountMode", CountMode.WholeRound, "Host: WholeRound = every event of the round counts (a new card can arrive already marked); FromDeath = only events after you died."); CompanyPool = file.Bind<bool>("Bingo", "CompanyPool", true, "Host: deal (3x3) cards on Company days, with Company squares (sales, quota)."); UseCoroner = file.Bind<bool>("Bingo", "UseCoroner", true, "Host: when the Coroner mod is installed, let its cause of death override our guess (no effect without Coroner)."); DeathRecap = file.Bind<bool>("Bingo", "DeathRecap", true, "Show 'Killed by: ...' above your card after you die."); PracticeBestMs = file.Bind<int>("Stats", "PracticeBestReactionMs", 0, "Your best solo practice reaction time (0 = none yet). Written by the mod."); DebugLog = file.Bind<bool>("Debug", "VerboseLogging", false, "Log protocol and session events."); } internal HostConfig BuildHostConfig(string version) { HostConfig hostConfig = new HostConfig { HostVersion = version }; hostConfig.Lobby.MinPlayers = MinPlayers.Value; hostConfig.Lobby.VoteMs = VoteSeconds.Value * 1000; List<GameId> list = new List<GameId>(); if (EnableTileFall.Value) { list.Add(GameId.TileFall); } if (EnableQteDuel.Value) { list.Add(GameId.QteDuel); } hostConfig.Lobby.EnabledGames = list; hostConfig.TileFall.MaxDurationTicks = TileFallMaxSeconds.Value * hostConfig.TileFall.TickRate; hostConfig.TileFall.SuddenDeathAfterTicks = hostConfig.TileFall.MaxDurationTicks / 2; return hostConfig; } internal SideConfig BuildSideConfig(bool betsAllowed) { SideConfig sideConfig = new SideConfig(); sideConfig.Bets.Enabled = BetsEnabled.Value && betsAllowed; sideConfig.Bets.Stakes = BetsConfig.ParseStakes(Stakes.Value); sideConfig.Bets.LateWindowMs = (int)(LateWindowSeconds.Value * 1000f); sideConfig.Bets.TogetherWindowMs = (int)(TogetherWindowSeconds.Value * 1000f); sideConfig.Bets.PropMarkets = PropMarkets.Value; sideConfig.Ecto.StartBalance = EctoStart.Value; sideConfig.Ecto.Floor = EctoFloor.Value; sideConfig.Ecto.TopUp = EctoTopUp.Value; sideConfig.Bingo.Enabled = BingoEnabled.Value; sideConfig.Bingo.CardSize = CardSize.Value; sideConfig.Bingo.CountMode = BingoCountMode.Value; sideConfig.Bingo.CompanyPool = CompanyPool.Value; sideConfig.Validate(); return sideConfig; } } [BepInPlugin("dev.thatumi.AfterlifeArcade", "AfterlifeArcade", "0.3.0")] [BepInDependency("LethalNetworkAPI", "3.4.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { private const string LnapiGuid = "LethalNetworkAPI"; private const string TestedLnapiVersion = "3.4.2"; private Harmony? _harmony; private void Awake() { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown LogBepInEx.Init(((BaseUnityPlugin)this).Logger); ArcadeConfig arcadeConfig; try { arcadeConfig = new ArcadeConfig(((BaseUnityPlugin)this).Config); Log.DebugEnabled = arcadeConfig.DebugLog.Value; } catch (Exception ex) { Log.Error("AfterlifeArcade config failed, plugin disabled: " + ex); return; } IArcadeInput input = null; try { input = ArcadeInputFactory.Create(arcadeConfig); } catch (Exception ex2) { Log.Error("AfterlifeArcade input setup failed (overlay keys disabled): " + ex2); } LogLnapiVersion(); ArcadeNet.Init(); ArcadeSession.Init(arcadeConfig, input, "0.3.0"); _harmony = new Harmony("dev.thatumi.AfterlifeArcade"); Patch(typeof(SpectatorInputPatches)); Patch(typeof(RoundEndPatches)); PatchSideGames(arcadeConfig); try { ArcadeRunner.Ensure(); } catch (Exception ex3) { Log.Error("AfterlifeArcade runner failed, plugin inactive: " + ex3); } Log.Info("AfterlifeArcade 0.3.0 loaded (" + CoreInfo.Describe() + ")"); } private static void LogLnapiVersion() { try { if (!Chainloader.PluginInfos.TryGetValue("LethalNetworkAPI", out var value) || ((value != null) ? value.Metadata : null) == null) { Log.Warn("AfterlifeArcade: LethalNetworkAPI plugin info not found; networking may not work."); return; } Version version = value.Metadata.Version; if (version.ToString() == "3.4.2") { Log.Info("Using LethalNetworkAPI " + version); } else { Log.Warn("AfterlifeArcade was tested with LethalNetworkAPI 3.4.2, this game runs " + version?.ToString() + ". If the arcade misbehaves, include this line in the report."); } } catch (Exception ex) { Log.Warn("AfterlifeArcade: could not read the LethalNetworkAPI version: " + ex.Message); } } private void PatchSideGames(ArcadeConfig cfg) { if (!cfg.BetsEnabled.Value && !cfg.BingoEnabled.Value) { Log.Info("AfterlifeArcade: bets and bingo are off in the config; their detectors are not patched"); } else if (RpcStage.Available) { Type[] array = new Type[11] { typeof(DeathPatches), typeof(SettlementPatches), typeof(LandminePatches), typeof(TurretPatches), typeof(RoundManagerPatches), typeof(StormyWeatherPatches), typeof(ShipTeleporterPatches), typeof(EntranceTeleportPatches), typeof(TimeOfDayPatches), typeof(EnemyPatches), typeof(CompanyPatches) }; foreach (Type patchClass in array) { Patch(patchClass); } } } private void Patch(Type patchClass) { try { _harmony.PatchAll(patchClass); } catch (Exception ex) { Log.Error("AfterlifeArcade: patching " + patchClass.Name + " failed, that feature is disabled: " + ex); } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "dev.thatumi.AfterlifeArcade"; public const string PLUGIN_NAME = "AfterlifeArcade"; public const string PLUGIN_VERSION = "0.3.0"; } } namespace AfterlifeArcade.Core { public enum Dir : byte { None, Up, Down, Left, Right } public static class DirExt { public static int Dx(this Dir d) { if (d != Dir.Left) { return (d == Dir.Right) ? 1 : 0; } return -1; } public static int Dy(this Dir d) { if (d != Dir.Up) { return (d == Dir.Down) ? 1 : 0; } return -1; } public static bool IsMove(this Dir d) { if ((int)d >= 1) { return (int)d <= 4; } return false; } } public enum GameId : byte { None, TileFall, QteDuel } public static class Games { public static readonly IReadOnlyList<GameId> All = new GameId[2] { GameId.TileFall, GameId.QteDuel }; public static bool IsKnown(GameId g) { if (g != GameId.TileFall) { return g == GameId.QteDuel; } return true; } public static string DisplayName(GameId g) { return g switch { GameId.TileFall => "Tile Fall", GameId.QteDuel => "Reaction Duel", _ => "?", }; } public static string Blurb(GameId g) { return g switch { GameId.TileFall => "Tiles crack under your feet. Keep moving. Last one standing wins.", GameId.QteDuel => "Press the shown key as fast as you can. Too early counts as a miss.", _ => "", }; } public static byte Mask(IEnumerable<GameId> games) { byte b = 0; foreach (GameId game in games) { if (IsKnown(game)) { b |= (byte)(1 << (int)game); } } return b; } public static List<GameId> FromMask(byte mask) { List<GameId> list = new List<GameId>(); foreach (GameId item in All) { if ((mask & (1 << (int)item)) != 0) { list.Add(item); } } return list; } } public readonly struct Placement : IEquatable<Placement> { public ulong Id { get; } public int Place { get; } public Placement(ulong id, int place) { Id = id; Place = place; } public bool Equals(Placement other) { if (Id == other.Id) { return Place == other.Place; } return false; } public override bool Equals(object? obj) { if (obj is Placement other) { return Equals(other); } return false; } public override int GetHashCode() { return HashCode.Combine(Id, Place); } public override string ToString() { return Id + "#" + Place; } } public enum EndReason : byte { None, ShipLeft, GameOver, Fired, NewRound, HostStopped, Revived, Disconnected } public static class Ranking { public static List<Placement> Compute<T>(IReadOnlyList<T> items, Func<T, ulong> id, Comparison<T> better, Func<T, bool> unranked) { List<T> list = new List<T>(); List<T> list2 = new List<T>(); foreach (T item in items) { if (unranked(item)) { list2.Add(item); } else { list.Add(item); } } for (int i = 1; i < list.Count; i++) { T val = list[i]; int num = i - 1; while (num >= 0 && better(val, list[num]) < 0) { list[num + 1] = list[num]; num--; } list[num + 1] = val; } List<Placement> list3 = new List<Placement>(items.Count); for (int j = 0; j < list.Count; j++) { int place = j + 1; if (j > 0 && better(list[j], list[j - 1]) == 0) { place = list3[j - 1].Place; } list3.Add(new Placement(id(list[j]), place)); } foreach (T item2 in list2) { list3.Add(new Placement(id(item2), 0)); } return list3; } } public static class CoreInfo { public const int FormatVersion = 2; public static string Describe() { return "AfterlifeArcade.Core protocol v" + 2; } } public sealed class Rng { private ulong _state; public Rng(ulong seed) { ulong x = seed; _state = SplitMix(ref x); if (_state == 0L) { _state = 11400714819323198485uL; } } public static ulong SplitMix(ref ulong x) { x += 11400714819323198485uL; ulong num = x; long num2 = (long)(num ^ (num >> 30)) * -4658895280553007687L; long num3 = (num2 ^ (num2 >>> 27)) * -7723592293110705685L; return (ulong)(num3 ^ (num3 >>> 31)); } public ulong NextULong() { _state ^= _state >> 12; _state ^= _state << 25; _state ^= _state >> 27; return _state * 2685821657736338717L; } public uint NextUInt() { return (uint)(NextULong() >> 32); } public int Next(int maxExclusive) { if (maxExclusive <= 0) { throw new ArgumentOutOfRangeException("maxExclusive"); } uint num = (uint)(-maxExclusive) % (uint)maxExclusive; uint num2; do { num2 = NextUInt(); } while (num2 < num); return (int)(num2 % (uint)maxExclusive); } public int Range(int minInclusive, int maxInclusive) { if (maxInclusive < minInclusive) { throw new ArgumentOutOfRangeException("maxInclusive"); } return minInclusive + Next(maxInclusive - minInclusive + 1); } public void Shuffle<T>(IList<T> list) { for (int num = list.Count - 1; num > 0; num--) { int index = Next(num + 1); T value = list[num]; list[num] = list[index]; list[index] = value; } } } public enum Waveform { Sine, Square, Triangle } public static class ToneSynth { public static float[] Tone(int sampleRate, float startHz, float endHz, float seconds, float volume, Waveform wave = Waveform.Sine, float attackSec = 0.005f, float releaseSec = 0.03f) { if (sampleRate < 1000 || sampleRate > 192000) { throw new ArgumentOutOfRangeException("sampleRate"); } if (seconds <= 0f || seconds > 5f) { throw new ArgumentOutOfRangeException("seconds"); } int num = Math.Max(1, (int)((float)sampleRate * seconds)); float[] array = new float[num]; double num2 = 0.0; volume = Math.Max(0f, Math.Min(1f, volume)); int num3 = Math.Max(1, (int)(attackSec * (float)sampleRate)); int num4 = Math.Max(1, (int)(releaseSec * (float)sampleRate)); for (int i = 0; i < num; i++) { double num5 = (double)i / (double)num; double num6 = (double)startHz + (double)(endHz - startHz) * num5; num2 += Math.PI * 2.0 * num6 / (double)sampleRate; double num7 = wave switch { Waveform.Square => (Math.Sin(num2) >= 0.0) ? 1 : (-1), Waveform.Triangle => 2.0 / Math.PI * Math.Asin(Math.Sin(num2)), _ => Math.Sin(num2), }; double num8 = Math.Min(1.0, Math.Min((double)i / (double)num3, (double)(num - 1 - i) / (double)num4)); array[i] = (float)(num7 * num8 * (double)volume); } return array; } public static float[] Concat(params float[][] parts) { int num = 0; float[][] array = parts; foreach (float[] array2 in array) { num += array2.Length; } float[] array3 = new float[num]; int num2 = 0; array = parts; foreach (float[] array4 in array) { Array.Copy(array4, 0, array3, num2, array4.Length); num2 += array4.Length; } return array3; } } } namespace AfterlifeArcade.Core.TileFall { public sealed class PlayoutClock { public double TickMs { get; } public double TargetDelayTicks { get; } public double ResyncTicks { get; } public bool Started { get; private set; } public int NewestTick { get; private set; } public double RenderTick { get; private set; } public int Resyncs { get; private set; } public PlayoutClock(double tickMs, double targetDelayTicks = 3.0, double resyncTicks = 40.0) { if (tickMs <= 0.0) { throw new ArgumentOutOfRangeException("tickMs"); } TickMs = tickMs; TargetDelayTicks = targetDelayTicks; ResyncTicks = resyncTicks; } public void OnSnapshot(int tick) { if (!Started) { Started = true; NewestTick = tick; RenderTick = Math.Max(0.0, (double)tick - TargetDelayTicks); } else if (tick > NewestTick) { NewestTick = tick; } } public void Advance(double dtMs) { if (Started && !(dtMs <= 0.0)) { double num = (double)NewestTick - RenderTick; if (num > TargetDelayTicks + ResyncTicks) { RenderTick = (double)NewestTick - TargetDelayTicks; Resyncs++; } else { double num2 = ((num > TargetDelayTicks + 3.0) ? 1.6 : ((num > TargetDelayTicks + 1.0) ? 1.25 : ((!(num < TargetDelayTicks - 1.0)) ? 1.0 : 0.85))); RenderTick = Math.Min(NewestTick, RenderTick + dtMs / TickMs * num2); } } } } public sealed class SnapshotBuffer { private readonly List<TileFallSnapshot> _items = new List<TileFallSnapshot>(); private readonly int _capacity; public int Count => _items.Count; public TileFallSnapshot? Newest { get { if (_items.Count != 0) { return _items[_items.Count - 1]; } return null; } } public SnapshotBuffer(int capacity = 64) { _capacity = Math.Max(2, capacity); } public bool Add(TileFallSnapshot s) { if (_items.Count > 0 && s.Tick <= _items[_items.Count - 1].Tick) { return false; } _items.Add(s); if (_items.Count > _capacity) { _items.RemoveAt(0); } return true; } public void Trim(double renderTick) { while (_items.Count > 2 && (double)_items[1].Tick <= renderTick) { _items.RemoveAt(0); } } public bool Sample(double renderTick, int slot, out float x, out float y, out bool alive) { x = (y = 0f); alive = false; if (_items.Count == 0) { return false; } TileFallSnapshot tileFallSnapshot = null; TileFallSnapshot tileFallSnapshot2 = null; foreach (TileFallSnapshot item in _items) { if ((double)item.Tick <= renderTick) { tileFallSnapshot = item; continue; } tileFallSnapshot2 = item; break; } if (tileFallSnapshot == null) { tileFallSnapshot = tileFallSnapshot2; } if (tileFallSnapshot2 == null) { tileFallSnapshot2 = tileFallSnapshot; } if (slot < 0 || slot >= tileFallSnapshot.Players.Length || slot >= tileFallSnapshot2.Players.Length) { return false; } TileFallPlayerState tileFallPlayerState = tileFallSnapshot.Players[slot]; TileFallPlayerState tileFallPlayerState2 = tileFallSnapshot2.Players[slot]; double num = ((tileFallSnapshot2.Tick == tileFallSnapshot.Tick) ? 1.0 : Clamp01((renderTick - (double)tileFallSnapshot.Tick) / (double)(tileFallSnapshot2.Tick - tileFallSnapshot.Tick))); if (Math.Max(Math.Abs(tileFallPlayerState.X - tileFallPlayerState2.X), Math.Abs(tileFallPlayerState.Y - tileFallPlayerState2.Y)) > 1) { TileFallPlayerState tileFallPlayerState3 = ((num >= 0.5) ? tileFallPlayerState2 : tileFallPlayerState); x = (int)tileFallPlayerState3.X; y = (int)tileFallPlayerState3.Y; } else { x = (float)((double)(int)tileFallPlayerState.X + (double)(tileFallPlayerState2.X - tileFallPlayerState.X) * num); y = (float)((double)(int)tileFallPlayerState.Y + (double)(tileFallPlayerState2.Y - tileFallPlayerState.Y) * num); } alive = ((num >= 1.0) ? tileFallPlayerState2.Alive : tileFallPlayerState.Alive); return true; } private static double Clamp01(double v) { if (!(v < 0.0)) { if (!(v > 1.0)) { return v; } return 1.0; } return 0.0; } } public sealed class LocalPredictor { private readonly struct Pending { public ushort Seq { get; } public Dir Dir { get; } public long SentMs { get; } public Pending(ushort seq, Dir dir, long sentMs) { Seq = seq; Dir = dir; SentMs = sentMs; } } private readonly List<Pending> _pending = new List<Pending>(); private TileFallSnapshot? _snap; private long _snapRecvMs; private int _slot = -1; public const int MaxPending = 6; public const double DefaultRttMs = 250.0; public const int SafetyTicks = 2; public double TickMs { get; } public int X { get; private set; } public int Y { get; private set; } public bool Alive { get; private set; } public bool HasState => _snap != null; public int PendingCount => _pending.Count; public int LastCorrection { get; private set; } public int MaxCorrection { get; private set; } public int DeathSnap { get; private set; } public double RttMs { get; private set; } public int RttSamples { get; private set; } public LocalPredictor(double tickMs = 50.0) { if (tickMs <= 0.0) { throw new ArgumentOutOfRangeException("tickMs"); } TickMs = tickMs; RttMs = 250.0; } public void Reset() { _pending.Clear(); _snap = null; _slot = -1; int num = (DeathSnap = 0); int lastCorrection = (MaxCorrection = num); LastCorrection = lastCorrection; RttMs = 250.0; RttSamples = 0; } public void OnSnapshot(TileFallSnapshot snap, int slot, long localNowMs = 0L) { if (slot < 0 || slot >= snap.Players.Length) { return; } bool flag = _snap != null; bool alive = Alive; int x = X; int y = Y; _snap = snap; _snapRecvMs = localNowMs; _slot = slot; TileFallPlayerState tileFallPlayerState = snap.Players[slot]; if (tileFallPlayerState.HasSeq) { for (int num = _pending.Count - 1; num >= 0; num--) { if (!TileFallSim.SeqNewer(_pending[num].Seq, tileFallPlayerState.LastSeq)) { if (_pending[num].Seq == tileFallPlayerState.LastSeq) { SampleRtt(localNowMs - _pending[num].SentMs); } _pending.RemoveAt(num); } } } if (!tileFallPlayerState.Alive) { _pending.Clear(); } Replay(); int num2 = (flag ? Math.Max(Math.Abs(X - x), Math.Abs(Y - y)) : 0); if (flag && alive && !Alive) { DeathSnap = num2; LastCorrection = 0; return; } LastCorrection = num2; if (LastCorrection > MaxCorrection) { MaxCorrection = LastCorrection; } } public double LeadTicks(long localNowMs) { return (RttMs + (double)Math.Max(0L, localNowMs - _snapRecvMs)) / TickMs + 2.0; } public bool CanMove(Dir dir, long localNowMs = 0L) { if (_snap == null || !Alive || !dir.IsMove() || _pending.Count >= 6) { return false; } int num = X + dir.Dx(); int num2 = Y + dir.Dy(); switch (_snap.TileAt(num, num2)) { case TileState.Fallen: return false; case TileState.Cracking: if ((double)(int)_snap.Timers[num2 * _snap.Width + num] <= LeadTicks(localNowMs)) { return false; } break; } return true; } public bool IsDoomed(int x, int y, long localNowMs) { if (_snap == null || x < 0 || y < 0 || x >= _snap.Width || y >= _snap.Height) { return false; } int num = y * _snap.Width + x; if (_snap.Tiles[num] == TileState.Cracking) { return (double)(int)_snap.Timers[num] <= LeadTicks(localNowMs); } return false; } public void OnInputSent(ushort seq, Dir dir, long localNowMs = 0L) { if (_snap != null) { _pending.Add(new Pending(seq, dir, localNowMs)); Apply(dir); } } private void SampleRtt(long sample) { if (sample >= 0 && sample <= 5000) { RttMs = ((RttSamples == 0) ? ((double)sample) : (RttMs * 0.8 + (double)sample * 0.2)); RttSamples++; } } private void Replay() { if (_snap == null || _slot < 0) { return; } TileFallPlayerState tileFallPlayerState = _snap.Players[_slot]; X = tileFallPlayerState.X; Y = tileFallPlayerState.Y; Alive = tileFallPlayerState.Alive; foreach (Pending item in _pending) { Apply(item.Dir); } } private void Apply(Dir dir) { if (_snap != null && Alive) { int x = X + dir.Dx(); int y = Y + dir.Dy(); if (_snap.TileAt(x, y) != TileState.Fallen) { X = x; Y = y; } } } } public sealed class MoveRepeater { private Dir _held; private Dir _buffered; private long _lastMoveMs; private bool _hasMoved; public int IntervalMs { get; } public MoveRepeater(int intervalMs = 170) { if (intervalMs < 1) { throw new ArgumentOutOfRangeException("intervalMs"); } IntervalMs = intervalMs; } public void Reset() { _held = (_buffered = Dir.None); _hasMoved = false; } public Dir Update(Dir held, Dir pressed, long nowMs) { if (pressed.IsMove()) { _buffered = pressed; } else if (held.IsMove() && held != _held) { _buffered = held; } _held = held; if (_hasMoved && nowMs - _lastMoveMs < IntervalMs) { return Dir.None; } Dir result; if (_buffered.IsMove()) { result = _buffered; _buffered = Dir.None; } else { if (!_held.IsMove()) { return Dir.None; } result = _held; } _lastMoveMs = nowMs; _hasMoved = true; return result; } public void Refund() { _hasMoved = false; } } public sealed class TileFallClientView { private ushort _nextSeq = 1; public IReadOnlyList<ulong> Participants { get; } public int LocalSlot { get; } public bool IsParticipant => LocalSlot >= 0; public PlayoutClock Clock { get; } public SnapshotBuffer Buffer { get; } = new SnapshotBuffer(); public LocalPredictor Predictor { get; } public MoveRepeater Repeater { get; } = new MoveRepeater(); public TileFallSnapshot? Latest => Buffer.Newest; public int SnapshotsReceived { get; private set; } public TileFallClientView(IReadOnlyList<ulong> participants, ulong localId, int tickMs) { Participants = participants; LocalSlot = -1; for (int i = 0; i < participants.Count; i++) { if (participants[i] == localId) { LocalSlot = i; } } Clock = new PlayoutClock(tickMs); Predictor = new LocalPredictor(tickMs); } public void OnSnapshot(TileFallSnapshot s, long localNowMs = 0L) { if (s.Players.Length == Participants.Count && Buffer.Add(s)) { SnapshotsReceived++; Clock.OnSnapshot(s.Tick); if (IsParticipant) { Predictor.OnSnapshot(s, LocalSlot, localNowMs); } } } public bool Update(double dtMs, long localNowMs, Dir held, Dir pressed, bool inputAllowed, out ushort seq, out Dir move) { seq = 0; move = Dir.None; Clock.Advance(dtMs); Buffer.Trim(Clock.RenderTick - 2.0); if (!IsParticipant || !inputAllowed || !Predictor.HasState || !Predictor.Alive) { Repeater.Reset(); return false; } Dir dir = Repeater.Update(held, pressed, localNowMs); if (!dir.IsMove()) { return false; } if (!Predictor.CanMove(dir, localNowMs)) { Repeater.Refund(); return false; } seq = _nextSeq++; if (_nextSeq == 0) { _nextSeq = 1; } move = dir; Predictor.OnInputSent(seq, dir, localNowMs); return true; } public bool GetDisplay(int slot, out float x, out float y, out bool alive) { if (slot == LocalSlot && Predictor.HasState) { x = Predictor.X; y = Predictor.Y; alive = Predictor.Alive; return true; } return Buffer.Sample(Clock.RenderTick, slot, out x, out y, out alive); } } public static class SnapshotCodec { public const int MaxPlayers = 8; public static void Write(PacketWriter w, TileFallSnapshot s) { if (s.Tick < 0 || s.Tick > 65535) { throw new ArgumentOutOfRangeException("s", "tick out of range"); } if (s.Players.Length > 8) { throw new ArgumentOutOfRangeException("s", "too many players"); } int num = s.Width * s.Height; if (s.Tiles.Length != num || s.Timers.Length != num) { throw new ArgumentException("tile arrays do not match width*height"); } w.UShort((ushort)s.Tick); w.Byte(s.Finished ? ((byte)1) : ((byte)0)); w.Byte((byte)s.Width); w.Byte((byte)s.Height); WriteMask(w, s.Tiles, TileState.Fallen); WriteMask(w, s.Tiles, TileState.Cracking); for (int i = 0; i < num; i++) { if (s.Tiles[i] == TileState.Cracking) { w.Byte(s.Timers[i]); } } w.Byte((byte)s.Players.Length); TileFallPlayerState[] players = s.Players; for (int j = 0; j < players.Length; j++) { TileFallPlayerState tileFallPlayerState = players[j]; w.Byte(tileFallPlayerState.X); w.Byte(tileFallPlayerState.Y); w.Byte((byte)((tileFallPlayerState.Alive ? 1u : 0u) | (uint)(tileFallPlayerState.HasSeq ? 2 : 0))); w.UShort(tileFallPlayerState.LastSeq); } } public static TileFallSnapshot Read(PacketReader r) { TileFallSnapshot tileFallSnapshot = new TileFallSnapshot { Tick = r.UShort() }; byte b = r.Byte(); if ((b & -2) != 0) { throw new PacketFormatException("unknown snapshot flags " + b); } tileFallSnapshot.Finished = (b & 1) != 0; tileFallSnapshot.Width = r.Byte(); tileFallSnapshot.Height = r.Byte(); if (tileFallSnapshot.Width < 3 || tileFallSnapshot.Width > 32 || tileFallSnapshot.Height < 3 || tileFallSnapshot.Height > 32) { throw new PacketFormatException("bad grid size"); } int num = tileFallSnapshot.Width * tileFallSnapshot.Height; bool[] array = ReadMask(r, num); bool[] array2 = ReadMask(r, num); tileFallSnapshot.Tiles = new TileState[num]; tileFallSnapshot.Timers = new byte[num]; for (int i = 0; i < num; i++) { if (array[i] && array2[i]) { throw new PacketFormatException("tile " + i + " both fallen and cracking"); } if (array[i]) { tileFallSnapshot.Tiles[i] = TileState.Fallen; } else if (array2[i]) { tileFallSnapshot.Tiles[i] = TileState.Cracking; } } for (int j = 0; j < num; j++) { if (tileFallSnapshot.Tiles[j] == TileState.Cracking) { tileFallSnapshot.Timers[j] = r.Byte(); if (tileFallSnapshot.Timers[j] == 0) { throw new PacketFormatException("cracking tile with zero timer"); } } } int num2 = r.Count(8); tileFallSnapshot.Players = new TileFallPlayerState[num2]; for (int k = 0; k < num2; k++) { TileFallPlayerState tileFallPlayerState = new TileFallPlayerState { X = r.Byte(), Y = r.Byte() }; byte b2 = r.Byte(); if ((b2 & -4) != 0) { throw new PacketFormatException("unknown player flags " + b2); } tileFallPlayerState.Alive = (b2 & 1) != 0; tileFallPlayerState.HasSeq = (b2 & 2) != 0; tileFallPlayerState.LastSeq = r.UShort(); if (tileFallPlayerState.X >= tileFallSnapshot.Width || tileFallPlayerState.Y >= tileFallSnapshot.Height) { throw new PacketFormatException("player outside grid"); } tileFallSnapshot.Players[k] = tileFallPlayerState; } return tileFallSnapshot; } public static byte[] Encode(TileFallSnapshot s) { PacketWriter packetWriter = new PacketWriter(96); Write(packetWriter, s); return packetWriter.ToArray(); } public static TileFallSnapshot Decode(byte[] data) { PacketReader packetReader = new PacketReader(data); TileFallSnapshot result = Read(packetReader); packetReader.ExpectEnd(); return result; } public static void WriteMask(PacketWriter w, TileState[] tiles, TileState state) { int num = (tiles.Length + 7) / 8; for (int i = 0; i < num; i++) { byte b = 0; for (int j = 0; j < 8; j++) { int num2 = i * 8 + j; if (num2 < tiles.Length && tiles[num2] == state) { b |= (byte)(1 << j); } } w.Byte(b); } } public static bool[] ReadMask(PacketReader r, int n) { bool[] array = new bool[n]; int num = (n + 7) / 8; for (int i = 0; i < num; i++) { byte b = r.Byte(); for (int j = 0; j < 8; j++) { int num2 = i * 8 + j; bool flag = (b & (1 << j)) != 0; if (num2 < n) { array[num2] = flag; } else if (flag) { throw new PacketFormatException("padding bit set in mask"); } } } return array; } } public enum TileState : byte { Intact, Cracking, Fallen } public sealed class TileFallConfig { public int Width { get; set; } = 10; public int Height { get; set; } = 10; public int TickRate { get; set; } = 20; public int CrackTicks { get; set; } = 12; public int MaxMovesPerSecond { get; set; } = 6; public int CoyoteTicks { get; set; } = 4; public int GraceTicks { get; set; } = 30; public int MaxDurationTicks { get; set; } = 1800; public int SuddenDeathAfterTicks { get; set; } = 900; public int SuddenDeathEveryTicks { get; set; } = 6; public int MaxQueuedInputs { get; set; } = 3; public int MaxPlayers { get; set; } = 8; public int TickMs => 1000 / TickRate; public int BucketCapacity => TickRate + MaxMovesPerSecond - 1; public TileFallConfig Clone() { return (TileFallConfig)MemberwiseClone(); } public void Validate() { if (Width < 3 || Width > 32) { throw new ArgumentException("Width must be 3..32"); } if (Height < 3 || Height > 32) { throw new ArgumentException("Height must be 3..32"); } if (TickRate < 5 || TickRate > 60 || 1000 % TickRate != 0) { throw new ArgumentException("TickRate must divide 1000 (5..60)"); } if (CrackTicks < 1 || CrackTicks > 255) { throw new ArgumentException("CrackTicks must be 1..255"); } if (MaxMovesPerSecond < 1 || MaxMovesPerSecond > TickRate) { throw new ArgumentException("MaxMovesPerSecond must be 1..TickRate"); } if (GraceTicks < 0 || MaxDurationTicks < 1 || MaxDurationTicks > 65535) { throw new ArgumentException("bad durations"); } if (SuddenDeathEveryTicks < 1) { throw new ArgumentException("SuddenDeathEveryTicks must be ≥ 1"); } if (CoyoteTicks < 0 || CoyoteTicks > 20) { throw new ArgumentException("CoyoteTicks must be 0..20"); } if (MaxQueuedInputs < 1 || MaxQueuedInputs > 16) { throw new ArgumentException("MaxQueuedInputs must be 1..16"); } if (MaxPlayers < 1 || MaxPlayers > 8) { throw new ArgumentException("MaxPlayers must be 1..8"); } } } public struct TileFallPlayerState : IEquatable<TileFallPlayerState> { public byte X; public byte Y; public bool Alive; public ushort LastSeq; public bool HasSeq; public bool Equals(TileFallPlayerState o) { if (X == o.X && Y == o.Y && Alive == o.Alive && LastSeq == o.LastSeq) { return HasSeq == o.HasSeq; } return false; } public override bool Equals(object? obj) { if (obj is TileFallPlayerState o) { return Equals(o); } return false; } public override int GetHashCode() { return HashCode.Combine(X, Y, Alive, LastSeq, HasSeq); } } public sealed class TileFallSnapshot { public int Tick; public int Width; public int Height; public bool Finished; public TileState[] Tiles = Array.Empty<TileState>(); public byte[] Timers = Array.Empty<byte>(); public TileFallPlayerState[] Players = Array.Empty<TileFallPlayerState>(); public TileState TileAt(int x, int y) { if (x >= 0 && y >= 0 && x < Width && y < Height) { return Tiles[y * Width + x]; } return TileState.Fallen; } public int AliveCount() { int num = 0; TileFallPlayerState[] players = Players; for (int i = 0; i < players.Length; i++) { if (players[i].Alive) { num++; } } return num; } } public sealed class TileFallSim { private readonly struct QueuedMove { public ushort Seq { get; } public Dir Dir { get; } public QueuedMove(ushort seq, Dir dir) { Seq = seq; Dir = dir; } } private sealed class Player { public readonly ulong Id; public readonly Queue<QueuedMove> Queue = new Queue<QueuedMove>(); public int X; public int Y; public bool Alive = true; public bool Left; public int EliminatedAtTick = -1; public int FallingSinceTick = -1; public int Tokens; public ushort LastSeq; public bool HasSeq; public ushort LastQueuedSeq; public bool HasQueuedSeq; public Player(ulong id, int x, int y, int tokens) { Id = id; X = x; Y = y; Tokens = tokens; } } private readonly TileFallConfig _cfg; private readonly TileState[] _tiles; private readonly byte[] _timers; private readonly Player[] _players; private readonly Rng _rng; private readonly List<int> _eliminatedThisTick = new List<int>(); public TileFallConfig Config => _cfg; public int Tick { get; private set; } public bool IsFinished { get; private set; } public int PlayerCount => _players.Length; public IReadOnlyList<int> EliminatedThisTick => _eliminatedThisTick; public TileFallSim(TileFallConfig config, ulong seed, IReadOnlyList<ulong> playerIds) { _cfg = (config ?? throw new ArgumentNullException("config")).Clone(); _cfg.Validate(); if (playerIds == null || playerIds.Count < 1 || playerIds.Count > _cfg.MaxPlayers) { throw new ArgumentException("1.." + _cfg.MaxPlayers + " players required"); } for (int i = 0; i < playerIds.Count; i++) { for (int j = 0; j < i; j++) { if (playerIds[i] == playerIds[j]) { throw new ArgumentException("duplicate player id " + playerIds[i]); } } } _rng = new Rng(seed); _tiles = new TileState[_cfg.Width * _cfg.Height]; _timers = new byte[_tiles.Length]; _players = new Player[playerIds.Count]; int[] array = SpawnPoints(_cfg.Width, _cfg.Height, playerIds.Count, _rng); for (int k = 0; k < _players.Length; k++) { _players[k] = new Player(playerIds[k], array[k] % _cfg.Width, array[k] / _cfg.Width, _cfg.BucketCapacity); } } public int SlotOf(ulong id) { for (int i = 0; i < _players.Length; i++) { if (_players[i].Id == id) { return i; } } return -1; } public ulong IdOf(int slot) { return _players[slot].Id; } public TileState GetTile(int x, int y) { return _tiles[y * _cfg.Width + x]; } public int GetTimer(int x, int y) { return _timers[y * _cfg.Width + x]; } public TileFallPlayerState GetPlayer(int slot) { Player player = _players[slot]; return new TileFallPlayerState { X = (byte)player.X, Y = (byte)player.Y, Alive = player.Alive, LastSeq = player.LastSeq, HasSeq = player.HasSeq }; } public int AliveCount() { int num = 0; Player[] players = _players; for (int i = 0; i < players.Length; i++) { if (players[i].Alive) { num++; } } return num; } public bool QueueInput(ulong playerId, ushort seq, Dir dir) { if (IsFinished || !dir.IsMove()) { return false; } int num = SlotOf(playerId); if (num < 0) { return false; } Player player = _players[num]; if (!player.Alive) { return false; } if (player.HasQueuedSeq && !SeqNewer(seq, player.LastQueuedSeq)) { return false; } player.HasQueuedSeq = true; player.LastQueuedSeq = seq; if (player.Queue.Count >= _cfg.MaxQueuedInputs) { player.Queue.Dequeue(); } player.Queue.Enqueue(new QueuedMove(seq, dir)); return true; } public void RemovePlayer(ulong playerId) { int num = SlotOf(playerId); if (num >= 0) { Player player = _players[num]; player.Left = true; if (player.Alive) { player.Alive = false; player.EliminatedAtTick = Tick; player.Queue.Clear(); } CheckFinished(); } } public void Step() { _eliminatedThisTick.Clear(); if (IsFinished) { return; } Tick++; int width = _cfg.Width; Player[] players = _players; foreach (Player player in players) { if (!player.Alive) { continue; } player.Tokens = Math.Min(player.Tokens + _cfg.MaxMovesPerSecond, _cfg.BucketCapacity); if (player.Queue.Count != 0 && player.Tokens >= _cfg.TickRate) { QueuedMove queuedMove = player.Queue.Dequeue(); player.Tokens -= _cfg.TickRate; player.LastSeq = queuedMove.Seq; player.HasSeq = true; int num = player.X + queuedMove.Dir.Dx(); int num2 = player.Y + queuedMove.Dir.Dy(); if (num >= 0 && num2 >= 0 && num < width && num2 < _cfg.Height && _tiles[num2 * width + num] != TileState.Fallen) { player.X = num; player.Y = num2; } } } for (int j = 0; j < _tiles.Length; j++) { if (_tiles[j] == TileState.Cracking) { _timers[j]--; if (_timers[j] == 0) { _tiles[j] = TileState.Fallen; } } } if (Tick > _cfg.GraceTicks) { players = _players; foreach (Player player2 in players) { if (player2.Alive) { Crack(player2.Y * width + player2.X); } } } if (Tick >= _cfg.SuddenDeathAfterTicks && (Tick - _cfg.SuddenDeathAfterTicks) % _cfg.SuddenDeathEveryTicks == 0) { int num3 = 0; for (int k = 0; k < _tiles.Length; k++) { if (_tiles[k] == TileState.Intact) { num3++; } } if (num3 > 0) { int num4 = _rng.Next(num3); for (int l = 0; l < _tiles.Length; l++) { if (_tiles[l] == TileState.Intact && num4-- == 0) { Crack(l); break; } } } } for (int m = 0; m < _players.Length; m++) { Player player3 = _players[m]; if (!player3.Alive) { continue; } if (_tiles[player3.Y * width + player3.X] != TileState.Fallen) { player3.FallingSinceTick = -1; continue; } if (player3.FallingSinceTick < 0) { player3.FallingSinceTick = Tick; } if (Tick - player3.FallingSinceTick >= _cfg.CoyoteTicks) { player3.Alive = false; player3.EliminatedAtTick = Tick; player3.Queue.Clear(); _eliminatedThisTick.Add(m); } } CheckFinished(); if (!IsFinished && Tick >= _cfg.MaxDurationTicks) { IsFinished = true; } } public TileFallSnapshot Snapshot() { TileFallSnapshot tileFallSnapshot = new TileFallSnapshot { Tick = Tick, Width = _cfg.Width, Height = _cfg.Height, Finished = IsFinished, Tiles = (TileState[])_tiles.Clone(), Timers = (byte[])_timers.Clone(), Players = new TileFallPlayerState[_players.Length] }; for (int i = 0; i < _players.Length; i++) { tileFallSnapshot.Players[i] = GetPlayer(i); } return tileFallSnapshot; } public List<Placement> Ranking() { int tieBreakTick = int.MaxValue; return AfterlifeArcade.Core.Ranking.Compute(_players, (Player p) => p.Id, (Player a, Player b) => (b.Alive ? tieBreakTick : b.EliminatedAtTick).CompareTo(a.Alive ? tieBreakTick : a.EliminatedAtTick), (Player p) => p.Left); } public static bool SeqNewer(ushort a, ushort b) { return (short)(a - b) > 0; } public static int[] SpawnPoints(int width, int height, int count, Rng rng) { List<int> list = new List<int>(); int num = ((width >= 6 && height >= 6) ? 1 : 0); for (int i = num; i < height - num; i++) { for (int j = num; j < width - num; j++) { list.Add(i * width + j); } } rng.Shuffle(list); List<int> list2 = new List<int> { list[0] }; while (list2.Count < count) { int item = -1; int num2 = -1; foreach (int item2 in list) { if (list2.Contains(item2)) { continue; } int num3 = int.MaxValue; foreach (int item3 in list2) { int num4 = Math.Abs(item2 % width - item3 % width) + Math.Abs(item2 / width - item3 / width); if (num4 < num3) { num3 = num4; } } if (num3 > num2) { num2 = num3; item = item2; } } list2.Add(item); } return list2.ToArray(); } private void Crack(int index) { if (_tiles[index] == TileState.Intact) { _tiles[index] = TileState.Cracking; _timers[index] = (byte)_cfg.CrackTicks; } } private void CheckFinished() { if (!IsFinished) { int num = AliveCount(); if ((_players.Length >= 2) ? (num <= 1) : (num == 0)) { IsFinished = true; } } } } } namespace AfterlifeArcade.Core.Side { public enum SideToastKind { Bingo, Bet, RoundResults } public sealed class SideToast { public SideToastKind Kind { get; } public string Title { get; } public string Body { get; } public bool Good { get; } public SideToast(SideToastKind kind, string title, string body, bool good) { Kind = kind; Title = title; Body = body; Good = good; } } public readonly struct PendingBet { public byte MarketId { get; } public byte Option { get; } public byte StakeIndex { get; } public long SentAtMs { get; } public PendingBet(byte marketId, byte option, byte stakeIndex, long sentAtMs) { MarketId = marketId; Option = option; StakeIndex = stakeIndex; SentAtMs = sentAtMs; } } public sealed class SideClient { public const int FeedSize = 8; public const int RequestCooldownMs = 1000; public const int PendingTimeoutMs = 3000; private readonly Action<Message> _send; private readonly List<GameEventRow> _feed = new List<GameEventRow>(); private readonly List<BingoWinMsg> _wins = new List<BingoWinMsg>(); private readonly List<BetSettledMsg> _settled = new List<BetSettledMsg>(); private readonly Queue<SideToast> _toasts = new Queue<SideToast>(); private readonly List<PendingBet> _pending = new List<PendingBet>(); private long _lastRequestMs = long.MinValue; private bool _resultsShown; public int LocalSlot { get; set; } = -1; public bool LocalDead { get; set; } public bool LineForTheLiving { get; set; } = true; public Func<int, string> NameOf { get; set; } = (int slot) => "Player " + slot; public byte RoundId { get; private set; } public BetBookMsg? Book { get; private set; } public BingoCard? Card { get; private set; } public ushort LastSeq { get; private set; } public IReadOnlyList<GameEventRow> Feed => _feed; public IReadOnlyList<BingoWinMsg> Wins => _wins; public IReadOnlyList<BetSettledMsg> SettledThisRound => _settled; public IReadOnlyList<PendingBet> Pending => _pending; public bool Available => Book != null; public bool BetsEnabled { get { if (Book != null) { return (Book.Flags & BookFlags.BetsEnabled) != 0; } return false; } } public bool BingoEnabled { get { if (Book != null) { return (Book.Flags & BookFlags.BingoEnabled) != 0; } return false; } } public bool BetsClosed { get { if (Book != null) { return (Book.Flags & (BookFlags.BetsClosed | BookFlags.RoundOver)) != 0; } return true; } } public bool RoundOver { get { if (Book != null) { return (Book.Flags & BookFlags.RoundOver) != 0; } return false; } } public GameEventRow? MyDeath { get; private set; } public int Requests { get; private set; } public int Gaps { get; private set; } public int Dropped { get; private set; } public int CardVersion { get; private set; } public int FeedVersion { get; private set; } public int MyBalance => (Book?.RowOf(LocalSlot)?.Balance).GetValueOrDefault(); public SideClient(Action<Message> send) { _send = send ?? throw new ArgumentNullException("send"); } public SideToast? TakeToast() { if (_toasts.Count <= 0) { return null; } return _toasts.Dequeue(); } public void Reset() { Book = null; ResetRound(0); _lastRequestMs = long.MinValue; _toasts.Clear(); } public bool RequestSnapshot(SideWhich which, long localMs) { if (which == SideWhich.None) { return false; } if (_lastRequestMs != long.MinValue && localMs - _lastRequestMs < 1000) { return false; } _las