Decompiled source of RepoTwitchIntegration by Schleimi v1.2.9

BepInEx/plugins/RepoTwitchIntegration/RepoTwitchIntegration.dll

Decompiled 17 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Photon.Pun;
using UnityEngine;

[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace RepoTwitchIntegration;

internal static class AppIdentity
{
	internal const string Name = "RepoIntegrationBySchieim";

	internal const string ClientId = "qajfmv5oa3cpcwsczxnv3q72c2u3l6";
}
public sealed class ChatMessage
{
	public string Id;

	public string UserId;

	public string Login;

	public string Text;

	public DateTime ReceivedAtUtc;
}
public sealed class ChatConnection
{
	private const string SocketUrl = "wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=30";

	private readonly TwitchApi api;

	private readonly TwitchAccount streamer;

	private readonly Action<ChatMessage> onMessage;

	private readonly Action<bool, string> onStatus;

	public ChatConnection(TwitchApi api, TwitchAccount streamer, Action<ChatMessage> onMessage, Action<bool, string> onStatus)
	{
		this.api = api;
		this.streamer = streamer;
		this.onMessage = onMessage;
		this.onStatus = onStatus;
	}

	public async Task Run(CancellationToken ct)
	{
		int retry = 2;
		while (!ct.IsCancellationRequested)
		{
			try
			{
				await RunSession(ct).ConfigureAwait(continueOnCapturedContext: false);
				retry = 2;
			}
			catch (OperationCanceledException)
			{
				if (ct.IsCancellationRequested)
				{
					break;
				}
				onStatus(arg1: false, "Chat connection timed out. Reconnecting ...");
			}
			catch (TwitchFailure twitchFailure)
			{
				onStatus(arg1: false, twitchFailure.Message);
				if (twitchFailure.Status == 401 || twitchFailure.Status == 403 || twitchFailure.Status == 400)
				{
					break;
				}
			}
			catch (Exception)
			{
				onStatus(arg1: false, "Chat connection interrupted. Retrying ...");
			}
			try
			{
				await Task.Delay(TimeSpan.FromSeconds(retry), ct).ConfigureAwait(continueOnCapturedContext: false);
			}
			catch (OperationCanceledException)
			{
				break;
			}
			retry = Math.Min(60, retry * 2);
		}
	}

	private async Task RunSession(CancellationToken ct)
	{
		ClientWebSocket current = null;
		try
		{
			string token = await streamer.GetAccess(ct).ConfigureAwait(continueOnCapturedContext: false);
			current = await Connect("wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=30", ct).ConfigureAwait(continueOnCapturedContext: false);
			JObject session = WelcomeSession(await Read(current, 15, ct).ConfigureAwait(continueOnCapturedContext: false));
			int timeout = ((int?)session["keepalive_timeout_seconds"]) ?? 30;
			TwitchApi twitchApi = api;
			string clientId = streamer.ClientId;
			JObject val = new JObject();
			val.Add("type", JToken.op_Implicit("channel.chat.message"));
			val.Add("version", JToken.op_Implicit("1"));
			JObject val2 = new JObject();
			val2.Add("broadcaster_user_id", JToken.op_Implicit(streamer.UserId));
			val2.Add("user_id", JToken.op_Implicit(streamer.UserId));
			val.Add("condition", (JToken)(object)val2);
			JObject val3 = new JObject();
			val3.Add("method", JToken.op_Implicit("websocket"));
			val3.Add("session_id", JToken.op_Implicit((string)session["id"]));
			val.Add("transport", (JToken)(object)val3);
			await twitchApi.Helix("eventsub/subscriptions", clientId, token, val, ct).ConfigureAwait(continueOnCapturedContext: false);
			onStatus(arg1: true, "Chat connected to #" + streamer.Login);
			while (!ct.IsCancellationRequested)
			{
				JObject envelope = await Read(current, timeout + 3, ct).ConfigureAwait(continueOnCapturedContext: false);
				switch ((string)envelope["metadata"][(object)"message_type"])
				{
				case "notification":
					Dispatch(envelope);
					break;
				case "session_reconnect":
				{
					string reconnectUrl = (string)envelope["payload"][(object)"session"][(object)"reconnect_url"];
					if (!Uri.TryCreate(reconnectUrl, UriKind.Absolute, out Uri uri) || uri.Scheme != "wss" || (!(uri.Host == "eventsub.wss.twitch.tv") && !uri.Host.EndsWith(".wss.twitch.tv", StringComparison.OrdinalIgnoreCase)))
					{
						throw new IOException("Invalid reconnect endpoint.");
					}
					using (CancellationTokenSource drainCancellation = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { ct }))
					{
						ClientWebSocket old = current;
						Task drain = Drain(old, timeout, drainCancellation.Token);
						ClientWebSocket replacement = null;
						try
						{
							replacement = await Connect(reconnectUrl, ct).ConfigureAwait(continueOnCapturedContext: false);
							JObject val4 = WelcomeSession(await Read(replacement, 15, ct).ConfigureAwait(continueOnCapturedContext: false));
							JObject next = val4;
							timeout = ((int?)next["keepalive_timeout_seconds"]) ?? timeout;
							current = replacement;
							replacement = null;
						}
						finally
						{
							drainCancellation.Cancel();
							old.Abort();
							old.Dispose();
							replacement?.Dispose();
						}
						await drain.ConfigureAwait(continueOnCapturedContext: false);
					}
					break;
				}
				case "revocation":
					throw new TwitchFailure(403, "revoked", "Twitch stopped chat reception. Please reconnect the streamer account.");
				}
			}
		}
		finally
		{
			current?.Dispose();
		}
	}

	private async Task Drain(ClientWebSocket socket, int timeout, CancellationToken ct)
	{
		int num = default(int);
		_ = num;
		_ = 0;
		try
		{
			while (!ct.IsCancellationRequested)
			{
				JObject message = await Read(socket, timeout + 3, ct).ConfigureAwait(continueOnCapturedContext: false);
				if ((string)message["metadata"][(object)"message_type"] == "notification")
				{
					Dispatch(message);
				}
			}
		}
		catch
		{
		}
	}

	private void Dispatch(JObject envelope)
	{
		//IL_003c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Expected O, but got Unknown
		if (!((string)envelope["metadata"][(object)"subscription_type"] != "channel.chat.message"))
		{
			JObject val = (JObject)envelope["payload"][(object)"event"];
			if (!((string)val["broadcaster_user_id"] != streamer.UserId))
			{
				onMessage(new ChatMessage
				{
					Id = (string)val["message_id"],
					UserId = (string)val["chatter_user_id"],
					Login = (string)val["chatter_user_login"],
					ReceivedAtUtc = DateTime.UtcNow,
					Text = (string)val["message"][(object)"text"]
				});
			}
		}
	}

	private static JObject WelcomeSession(JObject envelope)
	{
		//IL_0046: Unknown result type (might be due to invalid IL or missing references)
		//IL_004c: Expected O, but got Unknown
		if ((string)envelope["metadata"][(object)"message_type"] != "session_welcome")
		{
			throw new IOException("Missing welcome.");
		}
		return (JObject)envelope["payload"][(object)"session"];
	}

	private static async Task<ClientWebSocket> Connect(string url, CancellationToken ct)
	{
		ClientWebSocket socket = new ClientWebSocket();
		try
		{
			using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { ct });
			timeout.CancelAfter(TimeSpan.FromSeconds(15.0));
			await socket.ConnectAsync(new Uri(url), timeout.Token).ConfigureAwait(continueOnCapturedContext: false);
			return socket;
		}
		catch
		{
			socket.Dispose();
			throw;
		}
	}

	private static async Task<JObject> Read(ClientWebSocket socket, int timeoutSeconds, CancellationToken ct)
	{
		using CancellationTokenSource deadline = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { ct });
		using MemoryStream stream = new MemoryStream();
		deadline.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
		byte[] buffer = new byte[8192];
		WebSocketReceiveResult result;
		do
		{
			result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), deadline.Token).ConfigureAwait(continueOnCapturedContext: false);
			if (result.MessageType != WebSocketMessageType.Text)
			{
				throw new IOException("WebSocket closed or invalid frame.");
			}
			stream.Write(buffer, 0, result.Count);
			if (stream.Length > 1048576)
			{
				throw new IOException("Oversized EventSub message.");
			}
		}
		while (!result.EndOfMessage);
		return JObject.Parse(Encoding.UTF8.GetString(stream.ToArray()));
	}

	public static async Task Send(TwitchApi api, TwitchAccount sender, string broadcasterId, string text, CancellationToken ct)
	{
		string token = await sender.GetAccess(ct).ConfigureAwait(continueOnCapturedContext: false);
		string clientId = sender.ClientId;
		JObject val = new JObject();
		val.Add("broadcaster_id", JToken.op_Implicit(broadcasterId));
		val.Add("sender_id", JToken.op_Implicit(sender.UserId));
		val.Add("message", JToken.op_Implicit(text));
		JToken obj = (await api.Helix("chat/messages", clientId, token, val, ct).ConfigureAwait(continueOnCapturedContext: false))["data"];
		JArray data = (JArray)(object)((obj is JArray) ? obj : null);
		if (data == null || ((JContainer)data).Count != 1 || (bool?)data[0][(object)"is_sent"] != true)
		{
			throw new InvalidOperationException("Twitch did not send the message. Check chat rules / AutoMod.");
		}
	}
}
internal sealed class ChatEffects : IDisposable
{
	private sealed class DropTarget
	{
		internal PhysGrabObject Item;

		internal PhysGrabber Grabber;

		internal bool Confirmed;
	}

	private readonly ChatPoints points;

	private readonly Action<string> reply;

	private readonly ManualLogSource log;

	private readonly MonsterClaims messages = new MonsterClaims();

	private readonly ShopRollService shop;

	private readonly ExplosionService explosions;

	private readonly JumpscareService jumpscares;

	private LevelGenerator level;

	private double nextFailure;

	private double now;

	private readonly List<DropTarget> dropping = new List<DropTarget>();

	private string dropChannel;

	private double dropDeadline;

	private bool dropCredited;

	private readonly ViewerCooldown dropCooldown = new ViewerCooldown();

	private string dropViewer;

	private string previousChannel;

	internal string Channel;

	private static readonly FieldInfo ChargeFloat = AccessTools.Field(typeof(ChargingStation), "chargeFloat");

	private static readonly FieldInfo ChargeTotal = AccessTools.Field(typeof(ChargingStation), "chargeTotal");

	private static readonly FieldInfo LastSegment = AccessTools.Field(typeof(ChargingStation), "lastSentSegment");

	private static readonly FieldInfo CurrentSegment = AccessTools.Field(typeof(ChargingStation), "chargeSegmentCurrent");

	internal string Status { get; private set; }

	internal ChatEffects(ChatPoints points, Action<string> reply, ManualLogSource log)
	{
		this.points = points;
		this.reply = reply;
		this.log = log;
		shop = new ShopRollService(points, reply, log);
		Action<string> action = delegate(string text)
		{
			Say(text, success: true);
		};
		explosions = new ExplosionService(points, action, log);
		jumpscares = new JumpscareService(points);
		Status = "!jumpscare +5 (host audio, 3m/viewer) | !boom +20 | !drop +40 (per viewer: boom 5m, drop 10m) | !charge -100 | !reroll -100";
	}

	internal void Tick(double time)
	{
		now = time;
		shop.Channel = Channel;
		shop.Tick(time);
		explosions.Tick(Channel, time);
		if (!object.ReferenceEquals(level, LevelGenerator.Instance) || previousChannel != Channel || !EffectWorld.Ready())
		{
			dropping.Clear();
			level = LevelGenerator.Instance;
			previousChannel = Channel;
		}
		else
		{
			CheckDrop();
		}
	}

	internal void Handle(ChatMessage message, double time)
	{
		string text = EffectRules.Command(message.Text);
		if (text == null || !messages.AcceptMessage(message.Id) || string.IsNullOrEmpty(Channel) || string.IsNullOrEmpty(message.UserId) || (message.ReceivedAtUtc != default(DateTime) && DateTime.UtcNow - message.ReceivedAtUtc > TimeSpan.FromSeconds(10.0)))
		{
			return;
		}
		now = time;
		if (!EffectWorld.Ready())
		{
			Say("These commands require a loaded active level or shop before the run is lost.", success: false);
			return;
		}
		try
		{
			switch (text)
			{
			case "!drop":
				Drop(message.UserId);
				break;
			case "!jumpscare":
				Say(jumpscares.Start(Channel, message.UserId, time), success: true);
				break;
			case "!boom":
				Say(explosions.Start(Channel, message.UserId, time), success: false);
				break;
			case "!charge":
				Charge();
				break;
			default:
				Say(shop.Start(message.UserId, time), success: false);
				break;
			}
		}
		catch (Exception ex)
		{
			log.LogError((object)("Chat effect failed: " + ex));
			Say("The effect could not complete. Check the host log.", success: false);
		}
	}

	private void Drop(string userId)
	{
		if (dropping.Count > 0)
		{
			Say("The previous drop is still being confirmed.", success: false);
			return;
		}
		double num = dropCooldown.Remaining(Channel, userId, now);
		if (num > 0.0)
		{
			Say("Your drop cooldown: " + Math.Ceiling(num) + "s.", success: false);
			return;
		}
		HashSet<PhysGrabber> hashSet = new HashSet<PhysGrabber>();
		foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
		{
			if (EffectWorld.Alive(item) && (Object)(object)item.physGrabber != (Object)null)
			{
				hashSet.Add(item.physGrabber);
			}
		}
		PhysGrabObject[] array = Object.FindObjectsOfType<PhysGrabObject>();
		foreach (PhysGrabObject val in array)
		{
			foreach (PhysGrabber item2 in val.playerGrabbing)
			{
				if (hashSet.Contains(item2))
				{
					dropping.Add(new DropTarget
					{
						Item = val,
						Grabber = item2
					});
				}
			}
		}
		if (dropping.Count == 0)
		{
			Say("Nobody is holding a physical object. No points added.", success: false);
			return;
		}
		dropChannel = Channel;
		dropViewer = userId;
		dropCredited = false;
		dropDeadline = now + 5.0;
		foreach (DropTarget item3 in dropping)
		{
			PhotonView component = ((Component)item3.Item).GetComponent<PhotonView>();
			if (!GameManager.Multiplayer() || !((Object)(object)component == (Object)null))
			{
				item3.Grabber.OverrideGrabRelease(((Object)(object)component == (Object)null) ? (-1) : component.ViewID, 0.75f);
			}
		}
		CheckDrop();
	}

	private void CheckDrop()
	{
		if (dropping.Count == 0)
		{
			return;
		}
		int num = 0;
		foreach (DropTarget item in dropping)
		{
			if (!item.Confirmed && (Object)(object)item.Item != (Object)null && (Object)(object)item.Grabber != (Object)null && (!GameManager.Multiplayer() || ((Object)(object)item.Grabber.photonView != (Object)null && item.Grabber.photonView.Owner != null && !item.Grabber.photonView.Owner.IsInactive)) && !item.Grabber.grabbed && !item.Item.playerGrabbing.Contains(item.Grabber))
			{
				item.Confirmed = true;
			}
			if (item.Confirmed)
			{
				num++;
			}
		}
		if (num > 0 && !dropCredited)
		{
			points.Credit(dropChannel, 400);
			dropCredited = true;
			dropCooldown.Start(dropChannel, dropViewer, now, 600.0);
		}
		if (num == dropping.Count || now >= dropDeadline)
		{
			if (dropChannel == Channel)
			{
				Say((num > 0) ? ("Drop confirmed for " + num + " grabs. +" + 40 + " Chat Points total.") : "No drop confirmed. No points added.", success: true);
			}
			dropping.Clear();
		}
	}

	private void Charge()
	{
		ChargingStation instance = ChargingStation.instance;
		if (!SemiFunc.RunIsLevel() || (Object)(object)instance == (Object)null || !((Behaviour)instance).isActiveAndEnabled || (Object)(object)StatsManager.instance == (Object)null || ChargeFloat == null || ChargeTotal == null || LastSegment == null || CurrentSegment == null || instance.chargeSegments <= 0 || instance.chargeSegments > 255)
		{
			Say("Charging requires the active truck charging station in a level. No points spent.", success: false);
			return;
		}
		float num = (float)ChargeFloat.GetValue(instance);
		float num2 = EffectRules.Charged(num);
		if (!(num2 > num))
		{
			Say("The truck is full or unavailable. No points spent.", success: false);
			return;
		}
		PhotonView component = ((Component)instance).GetComponent<PhotonView>();
		if (GameManager.Multiplayer() && (Object)(object)component == (Object)null)
		{
			Say("Truck network object is unavailable.", success: false);
			return;
		}
		PointReservation pointReservation = points.Reserve(Channel, 100);
		if (pointReservation == null)
		{
			Say("Charging costs 100 Chat Points.", success: false);
			return;
		}
		bool success = false;
		try
		{
			ChargeFloat.SetValue(instance, num2);
			ChargeTotal.SetValue(instance, Mathf.FloorToInt(num2 * 100f));
			StatsManager.instance.runStats["chargingStationChargeTotal"] = Mathf.FloorToInt(num2 * 100f);
			success = true;
			int num3 = Mathf.CeilToInt(num2 * (float)instance.chargeSegments);
			CurrentSegment.SetValue(instance, num3);
			if (GameManager.Multiplayer())
			{
				PhotonNetwork.RemoveBufferedRPCs(component.ViewID, "ChargingStationSegmentChangedRPC", (int[])null);
				component.RPC("ChargingStationSegmentChangedRPC", (RpcTarget)4, new object[1] { (byte)num3 });
			}
			LastSegment.SetValue(instance, num3);
		}
		finally
		{
			pointReservation.Complete(success);
		}
		Say("Truck charged by " + ((num2 - num) * 100f).ToString("0.#", CultureInfo.InvariantCulture) + "% of maximum. 100 Chat Points spent.", success: true);
	}

	private void Say(string text, bool success)
	{
		Status = text;
		if (success || !(now < nextFailure))
		{
			if (!success)
			{
				nextFailure = now + 5.0;
			}
			reply("[REPO] " + text);
		}
	}

	public void Dispose()
	{
		explosions.Clear();
		dropping.Clear();
		shop.Dispose();
	}
}
public sealed class ChatOutbox
{
	private sealed class Item
	{
		public string Text;

		public double Expires;
	}

	private readonly Queue<Item> pending = new Queue<Item>();

	public int Count => pending.Count;

	public bool Enqueue(string text, double now)
	{
		Prune(now);
		if (string.IsNullOrEmpty(text) || pending.Count >= 24)
		{
			return false;
		}
		pending.Enqueue(new Item
		{
			Text = text,
			Expires = now + 20.0
		});
		return true;
	}

	public string Take(double now)
	{
		Prune(now);
		if (pending.Count != 0)
		{
			return pending.Dequeue().Text;
		}
		return null;
	}

	private void Prune(double now)
	{
		while (pending.Count > 0 && pending.Peek().Expires <= now)
		{
			pending.Dequeue();
		}
	}

	public void Clear()
	{
		pending.Clear();
	}
}
public sealed class ChatPoints
{
	private Dictionary<string, long> balances = new Dictionary<string, long>();

	private readonly string path;

	private readonly Action<string> error;

	private long runGeneration;

	public bool Persistent { get; private set; }

	public ChatPoints(string path, bool persistent, Action<string> error)
	{
		this.path = path;
		this.error = error;
		Persistent = persistent;
		if (!persistent || !File.Exists(path))
		{
			return;
		}
		try
		{
			Dictionary<string, long> dictionary = JsonConvert.DeserializeObject<Dictionary<string, long>>(File.ReadAllText(path));
			if (dictionary == null)
			{
				return;
			}
			foreach (KeyValuePair<string, long> item in dictionary)
			{
				if (ValidChannel(item.Key) && item.Value >= 0)
				{
					balances[item.Key] = item.Value;
				}
			}
		}
		catch (Exception e)
		{
			Report(e);
		}
	}

	private static bool ValidChannel(string channel)
	{
		if (string.IsNullOrEmpty(channel))
		{
			return false;
		}
		foreach (char c in channel)
		{
			if (c < '0' || c > '9')
			{
				return false;
			}
		}
		return true;
	}

	public long HpUnits(string channel)
	{
		if (channel == null || !balances.TryGetValue(channel, out var value))
		{
			return 0L;
		}
		return value;
	}

	public static string Format(long units)
	{
		return ((decimal)units / 10m).ToString("0.#", CultureInfo.GetCultureInfo("en-US"));
	}

	public string Display(string channel)
	{
		return Format(HpUnits(channel));
	}

	public bool CanSpend(string channel, int cost)
	{
		if (ValidChannel(channel) && cost > 0)
		{
			return HpUnits(channel) >= (long)cost * 10L;
		}
		return false;
	}

	public bool TrySpend(string channel, int cost)
	{
		if (!CanSpend(channel, cost))
		{
			return false;
		}
		balances[channel] -= (long)cost * 10L;
		Save();
		return true;
	}

	public PointReservation Reserve(string channel, int cost)
	{
		if (!TrySpend(channel, cost))
		{
			return null;
		}
		return new PointReservation(this, channel, cost, runGeneration);
	}

	internal void Refund(string channel, int cost, long generation)
	{
		checked
		{
			if (generation == runGeneration)
			{
				balances[channel] = HpUnits(channel) + unchecked((long)cost) * 10L;
				Save();
			}
		}
	}

	public void Credit(string channel, int maxHp)
	{
		if (!ValidChannel(channel) || maxHp <= 0)
		{
			throw new ArgumentException("Invalid chat point credit.");
		}
		balances[channel] = checked(HpUnits(channel) + maxHp);
		Save();
	}

	public void SetPersistent(bool persistent)
	{
		Persistent = persistent;
		Save();
	}

	public void ResetAll()
	{
		runGeneration++;
		balances.Clear();
		Save();
	}

	public void LostRun()
	{
		if (!Persistent)
		{
			runGeneration++;
			balances.Clear();
			Save();
		}
	}

	private void Save()
	{
		try
		{
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			string sourceFileName = path + ".tmp";
			File.WriteAllText(sourceFileName, JsonConvert.SerializeObject((object)balances, (Formatting)1));
			if (File.Exists(path))
			{
				File.Replace(sourceFileName, path, null, ignoreMetadataErrors: true);
			}
			else
			{
				File.Move(sourceFileName, path);
			}
		}
		catch (Exception e)
		{
			Report(e);
		}
	}

	private void Report(Exception e)
	{
		if (error != null)
		{
			error("Could not load/save chat points: " + e.GetType().Name);
		}
	}
}
public sealed class PointReservation
{
	private readonly ChatPoints pool;

	private readonly string channel;

	private readonly int cost;

	private readonly long generation;

	private bool settled;

	internal PointReservation(ChatPoints pool, string channel, int cost, long generation)
	{
		this.pool = pool;
		this.channel = channel;
		this.cost = cost;
		this.generation = generation;
	}

	public void Complete(bool success)
	{
		if (!settled)
		{
			settled = true;
			if (!success)
			{
				pool.Refund(channel, cost, generation);
			}
		}
	}
}
public sealed class ChatPolicy
{
	private double nextTest;

	private readonly HashSet<string> seen = new HashSet<string>();

	private readonly Queue<string> order = new Queue<string>();

	public bool TestEnabled { get; private set; }

	public int TestNumber { get; private set; }

	public void SetTest(bool enabled, double now)
	{
		if (enabled != TestEnabled)
		{
			TestEnabled = enabled;
			nextTest = now + 30.0;
			if (enabled)
			{
				TestNumber = 0;
			}
		}
	}

	public string Handle(string messageId, string userId, string streamerId, string text, double now)
	{
		if (string.IsNullOrEmpty(messageId) || !seen.Add(messageId))
		{
			return null;
		}
		order.Enqueue(messageId);
		if (order.Count > 2048)
		{
			seen.Remove(order.Dequeue());
		}
		if (string.IsNullOrEmpty(streamerId) || userId != streamerId)
		{
			return null;
		}
		switch ((text ?? "").Trim().ToLowerInvariant())
		{
		case "!repotest on":
		case "!repotest an":
		{
			bool testEnabled2 = TestEnabled;
			SetTest(enabled: true, now);
			if (!testEnabled2)
			{
				return "[REPO] Connection test started. A test message will appear every 30 seconds.";
			}
			return null;
		}
		case "!repotest off":
		case "!repotest aus":
		{
			bool testEnabled = TestEnabled;
			SetTest(enabled: false, now);
			if (!testEnabled)
			{
				return null;
			}
			return "[REPO] Connection test stopped.";
		}
		default:
			return null;
		}
	}

	public string Tick(double now, bool ready)
	{
		if (!TestEnabled)
		{
			return null;
		}
		if (!ready)
		{
			nextTest = now + 30.0;
			return null;
		}
		if (now < nextTest)
		{
			return null;
		}
		nextTest = now + 30.0;
		TestNumber++;
		return "[REPO] Twitch connection active. Test #" + TestNumber + " | " + DateTime.UtcNow.ToString("HH:mm:ss") + " UTC";
	}

	public static T SelectSender<T>(T streamer, T bot, bool useBot) where T : class
	{
		if (!useBot || bot == null)
		{
			return streamer;
		}
		return bot;
	}
}
public static class EffectRules
{
	public const int DropReward = 40;

	public const int ChargeCost = 100;

	public const int RerollCost = 100;

	public const int ExplosionReward = 20;

	public const float ExplosionForce = 50f;

	public const float ExplosionSeconds = 3f;

	public const double ExplosionCooldown = 300.0;

	public const double DropCooldown = 600.0;

	public const double JumpscareCooldown = 180.0;

	public const int JumpscareReward = 5;

	public static string Command(string text)
	{
		string text2 = (text ?? "").Trim().ToLowerInvariant();
		switch (text2)
		{
		default:
			return null;
		case "!drop":
		case "!charge":
		case "!reroll":
		case "!boom":
		case "!jumpscare":
			return text2;
		}
	}

	public static float Charged(float current)
	{
		if (!float.IsNaN(current) && !float.IsInfinity(current) && !(current < 0f) && !(current >= 1f))
		{
			return Math.Min(1f, current + 0.1f);
		}
		return current;
	}
}
public sealed class ShopRollLedger
{
	private readonly HashSet<string> used = new HashSet<string>();

	private string pending;

	public double NextAllowed { get; private set; }

	public bool Busy => pending != null;

	private static string Key(string channel, string user)
	{
		return channel + ":" + user;
	}

	public bool Used(string channel, string user)
	{
		return used.Contains(Key(channel, user));
	}

	public bool Begin(string channel, string user, double now)
	{
		if (string.IsNullOrEmpty(channel) || string.IsNullOrEmpty(user) || Busy || now < NextAllowed || Used(channel, user))
		{
			return false;
		}
		pending = Key(channel, user);
		return true;
	}

	public void Complete(bool success, double now)
	{
		if (pending != null)
		{
			if (success)
			{
				used.Add(pending);
				NextAllowed = now + 30.0;
			}
			pending = null;
		}
	}

	public void Reset()
	{
		used.Clear();
		pending = null;
		NextAllowed = 0.0;
	}
}
internal static class EffectWorld
{
	internal const BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	internal static readonly FieldInfo Dead = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo Changing = typeof(RunManager).GetField("waitToChangeScene", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo AllDead = typeof(RunManager).GetField("allPlayersDead", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	internal static bool Ready()
	{
		//IL_0083: Unknown result type (might be due to invalid IL or missing references)
		//IL_0088: 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_008b: Invalid comparison between Unknown and I4
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Invalid comparison between Unknown and I4
		if (Dead == null || Changing == null || AllDead == null || (Object)(object)GameManager.instance == (Object)null || !SemiFunc.IsMasterClientOrSingleplayer() || (Object)(object)RunManager.instance == (Object)null || (!SemiFunc.RunIsLevel() && !SemiFunc.RunIsShop()) || (Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated || (Object)(object)GameDirector.instance == (Object)null)
		{
			return false;
		}
		gameState currentState = GameDirector.instance.currentState;
		if (((int)currentState == 2 || (int)currentState == 7) && !(bool)Changing.GetValue(RunManager.instance))
		{
			return !(bool)AllDead.GetValue(RunManager.instance);
		}
		return false;
	}

	internal static bool Alive(PlayerAvatar p)
	{
		if ((Object)(object)p != (Object)null && (Object)(object)p.playerHealth != (Object)null && !p.playerHealth.isMenuAvatar && !(bool)Dead.GetValue(p))
		{
			if (GameManager.Multiplayer())
			{
				if ((Object)(object)p.photonView != (Object)null && p.photonView.Owner != null)
				{
					return !p.photonView.Owner.IsInactive;
				}
				return false;
			}
			return true;
		}
		return false;
	}
}
internal sealed class ExplosionService
{
	private sealed class Target
	{
		internal PlayerAvatar Player;

		internal PlayerTumble Tumble;

		internal bool Done;
	}

	private static readonly FieldInfo Tumble = AccessTools.Field(typeof(PlayerAvatar), "tumble");

	private static readonly FieldInfo Tumbling = AccessTools.Field(typeof(PlayerTumble), "isTumbling");

	private static readonly FieldInfo Force = AccessTools.Field(typeof(PlayerTumble), "tumbleForce");

	private readonly ChatPoints points;

	private readonly Action<string> reply;

	private readonly ManualLogSource log;

	private readonly List<Target> pending = new List<Target>();

	private LevelGenerator level;

	private string channel;

	private string viewer;

	private readonly ViewerCooldown cooldown = new ViewerCooldown();

	private double deadline;

	private int confirmed;

	private bool credited;

	internal ExplosionService(ChatPoints points, Action<string> reply, ManualLogSource log)
	{
		this.points = points;
		this.reply = reply;
		this.log = log;
	}

	internal string Start(string currentChannel, string userId, double now)
	{
		if (pending.Count > 0)
		{
			return "The previous explosion is still being confirmed.";
		}
		double num = cooldown.Remaining(currentChannel, userId, now);
		if (num > 0.0)
		{
			return "Your boom cooldown: " + Math.Ceiling(num) + "s.";
		}
		if (Tumble == null || Tumbling == null || Force == null)
		{
			return "Cauldron effect is unavailable in this game version. No points added.";
		}
		foreach (PlayerAvatar p in SemiFunc.PlayerGetAll())
		{
			if (EffectWorld.Alive(p))
			{
				object? value = Tumble.GetValue(p);
				PlayerTumble val = (PlayerTumble)((value is PlayerTumble) ? value : null);
				if ((Object)(object)val != (Object)null && !pending.Exists((Target t) => (Object)(object)t.Player == (Object)(object)p))
				{
					pending.Add(new Target
					{
						Player = p,
						Tumble = val
					});
				}
			}
		}
		if (pending.Count == 0)
		{
			return "No living player can receive the cauldron effect. No points added.";
		}
		channel = currentChannel;
		viewer = userId;
		level = LevelGenerator.Instance;
		deadline = now + 5.0;
		confirmed = 0;
		credited = false;
		foreach (Target item in pending)
		{
			try
			{
				item.Tumble.TumbleRequest(true, false);
			}
			catch (Exception ex)
			{
				item.Done = true;
				log.LogError((object)("Chat explosion request failed: " + ex));
			}
		}
		return "Cauldron explosion requested for all living players.";
	}

	internal void Tick(string currentChannel, double now)
	{
		//IL_00b4: 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_00ce: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d3: 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_00dc: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
		//IL_0102: Unknown result type (might be due to invalid IL or missing references)
		//IL_0107: 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)
		//IL_011d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0135: Unknown result type (might be due to invalid IL or missing references)
		//IL_013b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0155: Unknown result type (might be due to invalid IL or missing references)
		//IL_015a: Unknown result type (might be due to invalid IL or missing references)
		//IL_015c: Unknown result type (might be due to invalid IL or missing references)
		//IL_015e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0160: Unknown result type (might be due to invalid IL or missing references)
		//IL_0165: Unknown result type (might be due to invalid IL or missing references)
		//IL_01cf: Unknown result type (might be due to invalid IL or missing references)
		if (pending.Count == 0)
		{
			return;
		}
		if (channel != currentChannel || !object.ReferenceEquals(level, LevelGenerator.Instance) || !EffectWorld.Ready())
		{
			pending.Clear();
			return;
		}
		bool flag = true;
		foreach (Target item in pending)
		{
			if (item.Done)
			{
				continue;
			}
			if (!EffectWorld.Alive(item.Player) || (Object)(object)item.Tumble == (Object)null)
			{
				item.Done = true;
				continue;
			}
			if (!(bool)Tumbling.GetValue(item.Tumble))
			{
				flag = false;
				continue;
			}
			item.Done = true;
			try
			{
				Vector3 val = Vector3.up + ((Component)item.Player).transform.forward * 0.25f;
				Vector3 normalized = ((Vector3)(ref val)).normalized;
				Vector3 val2 = Vector3.Cross(normalized, -((Component)item.Player).transform.up) * 50f;
				Vector3 val3 = (Vector3)Force.GetValue(item.Tumble);
				item.Tumble.TumbleOverrideTime(3f);
				item.Tumble.TumbleForce(normalized * 50f);
				Vector3 val4 = (Vector3)Force.GetValue(item.Tumble);
				Vector3 val5 = val4 - val3;
				if (((Vector3)(ref val5)).sqrMagnitude > 1f)
				{
					confirmed++;
					if (!credited)
					{
						credited = true;
						cooldown.Start(channel, viewer, now, 300.0);
						points.Credit(channel, 200);
					}
				}
				item.Tumble.TumbleTorque(val2);
			}
			catch (Exception ex)
			{
				log.LogError((object)("Chat cauldron launch failed: " + ex));
			}
		}
		if (flag || now >= deadline)
		{
			reply((confirmed > 0) ? ("Cauldron launch confirmed for " + confirmed + "/" + pending.Count + " players. +" + 20 + " Chat Points total.") : "No cauldron launch confirmed. No points added.");
			log.LogInfo((object)("Chat explosion: " + confirmed + "/" + pending.Count + " native force confirmations."));
			pending.Clear();
		}
	}

	internal void Clear()
	{
		pending.Clear();
	}
}
internal sealed class HealHooks : IDisposable
{
	private static readonly FieldInfo Health = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static HealHooks current;

	private readonly Harmony harmony = new Harmony("schleimi.repotwitchintegration.heal");

	private readonly Action<PlayerHealth> changed;

	private readonly Action<string> error;

	public HealHooks(Action<PlayerHealth> changed, Action<string> error)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Expected O, but got Unknown
		//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c5: Expected O, but got Unknown
		//IL_00c5: Expected O, but got Unknown
		this.changed = changed;
		this.error = error;
		if (Health == null)
		{
			throw new InvalidOperationException("Health tracking is not supported by this game version.");
		}
		try
		{
			string[] array = new string[2] { "Heal", "UpdateHealthRPC" };
			foreach (string text in array)
			{
				MethodInfo methodInfo = AccessTools.Method(typeof(PlayerHealth), text, (Type[])null, (Type[])null);
				if (methodInfo == null)
				{
					throw new InvalidOperationException("Missing health method: " + text);
				}
				harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(HealHooks), "Before", (Type[])null), new HarmonyMethod(typeof(HealHooks), "After", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			current = this;
		}
		catch
		{
			harmony.UnpatchSelf();
			throw;
		}
	}

	private static void Before(PlayerHealth __instance, out int __state)
	{
		__state = (int)Health.GetValue(__instance);
	}

	private static void After(PlayerHealth __instance, int __state)
	{
		if (current == null || (int)Health.GetValue(__instance) <= __state)
		{
			return;
		}
		try
		{
			current.changed(__instance);
		}
		catch (Exception ex)
		{
			current.error("Heal tracking failed: " + ex.GetType().Name);
		}
	}

	public void Dispose()
	{
		if (current == this)
		{
			current = null;
		}
		harmony.UnpatchSelf();
	}
}
public static class HealPolicy
{
	public const int Cost = 10;

	public const int Amount = 10;

	public static bool IsCommand(string text)
	{
		string a = (text ?? "").Trim();
		return string.Equals(a, "!heal", StringComparison.OrdinalIgnoreCase);
	}

	public static bool Eligible(bool dead, bool disabled, int health, int maxHealth)
	{
		if (!dead && !disabled && health > 0)
		{
			return health < maxHealth;
		}
		return false;
	}
}
internal sealed class HealService : IDisposable
{
	private sealed class Target
	{
		public PlayerAvatar Player;

		public PlayerHealth Health;

		public bool Sent;

		public bool Done;
	}

	private sealed class Purchase
	{
		public readonly List<Target> Targets = new List<Target>();

		public PointReservation Reservation;

		public LevelGenerator Level;

		public string Channel;

		public double Deadline;

		public int Confirmed;
	}

	private const BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	private static readonly FieldInfo Health = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo MaxHealth = typeof(PlayerHealth).GetField("maxHealth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo Dead = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo Disabled = typeof(PlayerAvatar).GetField("isDisabled", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo AllDead = typeof(RunManager).GetField("allPlayersDead", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static readonly FieldInfo Changing = typeof(RunManager).GetField("waitToChangeScene", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private readonly ChatPoints points;

	private readonly Action<string> reply;

	private readonly ManualLogSource log;

	private readonly MonsterClaims messages = new MonsterClaims();

	private HealHooks hooks;

	private Purchase pending;

	private double nextFailure;

	public string Channel { get; set; }

	public string Status { get; private set; }

	public HealService(ChatPoints points, Action<string> reply, ManualLogSource log)
	{
		this.points = points;
		this.reply = reply;
		this.log = log;
		try
		{
			if (Health == null || MaxHealth == null || Dead == null || Disabled == null || AllDead == null || Changing == null)
			{
				throw new InvalidOperationException("Required health fields were not found.");
			}
			hooks = new HealHooks(Observe, delegate(string text)
			{
				log.LogWarning((object)text);
			});
			Status = "!heal: +10 HP for all living injured players, 10 Chat Points total.";
		}
		catch (Exception ex)
		{
			Status = "Healing unavailable: " + ex.Message;
			log.LogError((object)Status);
		}
	}

	public void Handle(ChatMessage message, double now)
	{
		if (!HealPolicy.IsCommand(message.Text) || !messages.AcceptMessage(message.Id) || string.IsNullOrEmpty(message.UserId) || string.IsNullOrEmpty(Channel) || (message.ReceivedAtUtc != default(DateTime) && DateTime.UtcNow - message.ReceivedAtUtc > TimeSpan.FromSeconds(10.0)))
		{
			return;
		}
		if (hooks == null)
		{
			Reject("Healing is unavailable in this game version. No points spent.", now);
			return;
		}
		if (pending != null)
		{
			Reject("The previous heal is still being confirmed. Please wait a moment.", now);
			return;
		}
		if (!Ready())
		{
			Reject("Healing requires a fully loaded active level or shop, before the run is lost.", now);
			return;
		}
		Purchase purchase = new Purchase();
		purchase.Channel = Channel;
		purchase.Level = LevelGenerator.Instance;
		purchase.Deadline = now + 6.0;
		Purchase purchase2 = purchase;
		foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
		{
			if (Eligible(item) && (!GameManager.Multiplayer() || (!((Object)(object)item.photonView == (Object)null) && item.photonView.Owner != null && !item.photonView.Owner.IsInactive)))
			{
				purchase2.Targets.Add(new Target
				{
					Player = item,
					Health = item.playerHealth
				});
			}
		}
		if (purchase2.Targets.Count == 0)
		{
			Reject("No living player needs healing. No points spent.", now);
			return;
		}
		purchase2.Reservation = points.Reserve(Channel, 10);
		if (purchase2.Reservation == null)
		{
			Reject("Healing costs 10 Chat Points. Current pool: " + points.Display(Channel) + ".", now);
			return;
		}
		pending = purchase2;
		foreach (Target target in purchase2.Targets)
		{
			if (!Eligible(target.Player))
			{
				target.Done = true;
				continue;
			}
			target.Sent = true;
			try
			{
				target.Health.HealOther(10, true);
			}
			catch (Exception ex)
			{
				target.Done = true;
				log.LogError((object)("Native chat heal failed: " + ex));
			}
		}
		Status = "Healing requested; waiting for native health confirmation ...";
		Tick(now);
	}

	private static bool Eligible(PlayerAvatar player)
	{
		if ((Object)(object)player == (Object)null || (Object)(object)player.playerHealth == (Object)null || player.playerHealth.isMenuAvatar)
		{
			return false;
		}
		return HealPolicy.Eligible((bool)Dead.GetValue(player), (bool)Disabled.GetValue(player), (int)Health.GetValue(player.playerHealth), (int)MaxHealth.GetValue(player.playerHealth));
	}

	private static bool Ready()
	{
		//IL_005d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0063: Invalid comparison between Unknown and I4
		if ((Object)(object)GameManager.instance != (Object)null && SemiFunc.IsMasterClientOrSingleplayer() && (Object)(object)RunManager.instance != (Object)null && (SemiFunc.RunIsLevel() || SemiFunc.RunIsShop()) && (Object)(object)LevelGenerator.Instance != (Object)null && LevelGenerator.Instance.Generated && (Object)(object)GameDirector.instance != (Object)null && (int)GameDirector.instance.currentState == 2 && !(bool)AllDead.GetValue(RunManager.instance))
		{
			return !(bool)Changing.GetValue(RunManager.instance);
		}
		return false;
	}

	private void Observe(PlayerHealth health)
	{
		if (pending == null || !object.ReferenceEquals(pending.Level, LevelGenerator.Instance) || !Ready())
		{
			return;
		}
		foreach (Target target in pending.Targets)
		{
			if (target.Sent && !target.Done && !((Object)(object)target.Health != (Object)(object)health))
			{
				target.Done = true;
				pending.Confirmed++;
				pending.Reservation.Complete(success: true);
				break;
			}
		}
	}

	public void Tick(double now)
	{
		if (pending == null)
		{
			return;
		}
		bool flag = true;
		foreach (Target target in pending.Targets)
		{
			if (!target.Done)
			{
				flag = false;
				break;
			}
		}
		if (flag || !(now < pending.Deadline) || !object.ReferenceEquals(pending.Level, LevelGenerator.Instance) || !Ready())
		{
			Finish();
		}
	}

	private void Finish()
	{
		Purchase purchase = pending;
		pending = null;
		purchase.Reservation.Complete(purchase.Confirmed > 0);
		Status = ((purchase.Confirmed > 0) ? ("Heal confirmed for " + purchase.Confirmed + "/" + purchase.Targets.Count + " players: up to +10 HP each. 10 Chat Points spent.") : "Healing was not confirmed. No heal charge is kept.");
		if (purchase.Channel == Channel)
		{
			reply("[REPO] " + Status + " Pool: " + points.Display(purchase.Channel) + ".");
		}
		log.LogInfo((object)Status);
	}

	private void Reject(string text, double now)
	{
		Status = text;
		if (!(now < nextFailure))
		{
			nextFailure = now + 5.0;
			reply("[REPO] " + text);
		}
	}

	public void Dispose()
	{
		if (pending != null)
		{
			Finish();
		}
		if (hooks != null)
		{
			hooks.Dispose();
		}
	}
}
internal sealed class JumpscareService
{
	private sealed class Choice
	{
		internal AudioClip Clip;

		internal float Volume;
	}

	private readonly ChatPoints points;

	private readonly ViewerCooldown cooldown = new ViewerCooldown();

	private readonly Random random = new Random();

	internal JumpscareService(ChatPoints points)
	{
		this.points = points;
	}

	internal string Start(string channel, string viewer, double now)
	{
		//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
		//IL_01f3: Expected O, but got Unknown
		if (!EffectWorld.Ready() || !SemiFunc.RunIsLevel())
		{
			return "Jumpscare requires a loaded active level. No points added.";
		}
		double num = cooldown.Remaining(channel, viewer, now);
		if (num > 0.0)
		{
			return "Your jumpscare cooldown: " + Math.Ceiling(num) + "s.";
		}
		AudioManager instance = AudioManager.instance;
		if ((Object)(object)instance == (Object)null || (Object)(object)instance.SoundMasterGroup == (Object)null)
		{
			return "Game audio is unavailable. No points added.";
		}
		List<Choice> list = new List<Choice>();
		HashSet<AudioClip> seen = new HashSet<AudioClip>();
		Collect(typeof(EnemyUpscreamAnim), new string[1] { "sfxAttackLocal" }, list, seen);
		Collect(typeof(EnemyRobeAnim), new string[1] { "sfxAttack" }, list, seen);
		Collect(typeof(EnemyHunter), new string[1] { "soundShoot" }, list, seen);
		Object[] array = Resources.FindObjectsOfTypeAll(typeof(EnemyHunterAnim));
		foreach (Object target in array)
		{
			object obj = Read(target, "soundAimStart");
			Sound val = (Sound)((obj is Sound) ? obj : null);
			AddClips(Read(target, "aimStartClips") as AudioClip[], val?.Volume ?? 0f, list, seen);
		}
		Object[] array2 = Resources.FindObjectsOfTypeAll(typeof(EnemyBang));
		foreach (Object target2 in array2)
		{
			object target3 = Read(target2, "particleScriptExplosion");
			object target4 = Read(target3, "explosionPreset");
			object obj2 = Read(target4, "explosionSoundBig");
			Sound val2 = (Sound)((obj2 is Sound) ? obj2 : null);
			if (val2 != null)
			{
				AddClips(val2.Sounds, val2.Volume, list, seen);
			}
		}
		if (list.Count == 0)
		{
			return "No Vanilla monster voice clips are loaded. No points added.";
		}
		Choice choice = list[random.Next(list.Count)];
		GameObject val3 = new GameObject("Twitch Monster Jumpscare");
		try
		{
			AudioSource val4 = val3.AddComponent<AudioSource>();
			val4.playOnAwake = false;
			val4.loop = false;
			val4.clip = choice.Clip;
			val4.outputAudioMixerGroup = instance.SoundMasterGroup;
			val4.spatialBlend = 0f;
			val4.volume = choice.Volume;
			val4.pitch = 1f;
			val4.PlayOneShot(choice.Clip, 2f);
			if (!val4.isPlaying)
			{
				Object.Destroy((Object)(object)val3);
				return "No jumpscare playback confirmed. No points added.";
			}
			Object.Destroy((Object)(object)val3, choice.Clip.length + 1f);
			cooldown.Start(channel, viewer, now, 180.0);
			points.Credit(channel, 50);
			return "Monster jumpscare played for the host / stream. +5 Chat Points.";
		}
		catch
		{
			Object.Destroy((Object)(object)val3);
			throw;
		}
	}

	private static void Collect(Type type, string[] names, List<Choice> choices, HashSet<AudioClip> seen)
	{
		Object[] array = Resources.FindObjectsOfTypeAll(type);
		foreach (Object target in array)
		{
			foreach (string name in names)
			{
				object obj = Read(target, name);
				Sound val = (Sound)((obj is Sound) ? obj : null);
				if (val != null)
				{
					AddClips(val.Sounds, val.Volume, choices, seen);
				}
			}
		}
	}

	private static object Read(object target, string name)
	{
		if (target == null)
		{
			return null;
		}
		FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (!(field == null))
		{
			return field.GetValue(target);
		}
		return null;
	}

	private static void AddClips(AudioClip[] clips, float volume, List<Choice> choices, HashSet<AudioClip> seen)
	{
		if (clips == null || volume <= 0f || float.IsNaN(volume))
		{
			return;
		}
		foreach (AudioClip val in clips)
		{
			if ((Object)(object)val != (Object)null && val.length > 0f && val.length <= 12f && seen.Add(val))
			{
				choices.Add(new Choice
				{
					Clip = val,
					Volume = Mathf.Clamp01(volume)
				});
			}
		}
	}
}
internal sealed class KillAttribution : IDisposable
{
	private sealed class ThrownObject
	{
		internal int Enemy;

		internal float Expires;
	}

	private sealed class HitScope
	{
		internal Enemy Enemy;

		internal PlayerAvatar Player;
	}

	private static KillAttribution active;

	private readonly Harmony harmony = new Harmony("schleimi.repotwitchintegration.attackcredit");

	private readonly KillCreditWindow<PlayerAvatar> indirect = new KillCreditWindow<PlayerAvatar>();

	private readonly KillCreditWindow<PlayerAvatar> contact = new KillCreditWindow<PlayerAvatar>();

	private readonly Action<string> log;

	private LevelGenerator level;

	private string channel;

	private readonly Dictionary<PhysGrabObjectImpactDetector, ThrownObject> thrown = new Dictionary<PhysGrabObjectImpactDetector, ThrownObject>();

	private static int impactEnemy = -1;

	private static HurtCollider checkingCollider;

	private static Enemy hurtEnemy;

	private static PlayerAvatar hurtPlayer;

	private static readonly BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	internal KillAttribution(Action<string> log)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Expected O, but got Unknown
		//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f4: Expected O, but got Unknown
		//IL_00f4: Expected O, but got Unknown
		//IL_0121: Unknown result type (might be due to invalid IL or missing references)
		//IL_0138: Unknown result type (might be due to invalid IL or missing references)
		//IL_0143: Expected O, but got Unknown
		//IL_0143: Expected O, but got Unknown
		//IL_018a: Unknown result type (might be due to invalid IL or missing references)
		//IL_01a1: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ac: Expected O, but got Unknown
		//IL_01ac: Expected O, but got Unknown
		//IL_0252: Unknown result type (might be due to invalid IL or missing references)
		//IL_0269: Unknown result type (might be due to invalid IL or missing references)
		//IL_0274: Expected O, but got Unknown
		//IL_0274: Expected O, but got Unknown
		this.log = log;
		try
		{
			Patch(typeof(EnemyUpscreamAnim), "AttackImpulse", "Attack");
			Patch(typeof(EnemyFloaterAnim), "Attack", "Attack");
			Patch(typeof(FloaterAttackLogic), "StateSmash", "FloaterSmash");
			Patch(typeof(EnemyBowtieAnim), "YellStart", "Attack");
			harmony.Patch((MethodBase)AccessTools.Method(typeof(EnemyValuableThrower), "Throw", (Type[])null, (Type[])null), new HarmonyMethod(typeof(KillAttribution), "BeforeThrow", (Type[])null), new HarmonyMethod(typeof(KillAttribution), "AfterThrow", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			harmony.Patch((MethodBase)AccessTools.Method(typeof(PhysGrabObjectImpactDetector), "OnCollisionStay", (Type[])null, (Type[])null), new HarmonyMethod(typeof(KillAttribution), "BeforeImpact", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(KillAttribution), "EndImpact", (Type[])null), (HarmonyMethod)null);
			Patch(typeof(PlayerHealth), "HurtOther", "ObjectDamage");
			harmony.Patch((MethodBase)AccessTools.Method(typeof(HurtCollider), "PlayerHurt", (Type[])null, (Type[])null), new HarmonyMethod(typeof(KillAttribution), "BeforeHit", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(KillAttribution), "EndHit", (Type[])null), (HarmonyMethod)null);
			Patch(typeof(PlayerHealth), "Hurt", "DirectDamage");
			Patch(typeof(PlayerAvatar), "ReviveRPC", "ClearPlayer");
			Type type = null;
			Type[] nestedTypes = typeof(HurtCollider).GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic);
			foreach (Type type2 in nestedTypes)
			{
				if (type2.Name.StartsWith("<ColliderCheck>"))
				{
					type = type2;
				}
			}
			if (type == null)
			{
				throw new InvalidOperationException("Missing collider iterator.");
			}
			harmony.Patch((MethodBase)AccessTools.Method(type, "MoveNext", (Type[])null, (Type[])null), new HarmonyMethod(typeof(KillAttribution), "BeforeColliderCheck", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(KillAttribution), "EndColliderCheck", (Type[])null), (HarmonyMethod)null);
			Patch(typeof(HurtCollider), "PhysObjectHurt", "BeforeBodyHit");
			Patch(typeof(PhysGrabObjectImpactDetector), "DestroyObject", "BeforeBodyDestroy");
			active = this;
		}
		catch
		{
			harmony.UnpatchSelf();
			throw;
		}
	}

	private void Patch(Type type, string method, string prefix)
	{
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Expected O, but got Unknown
		MethodInfo methodInfo = AccessTools.Method(type, method, (Type[])null, (Type[])null);
		if (methodInfo == null)
		{
			throw new InvalidOperationException("Missing attack tracking method: " + type.Name + "." + method);
		}
		harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(KillAttribution), prefix, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
	}

	internal void Tick(string currentChannel)
	{
		if (channel != currentChannel || !object.ReferenceEquals(level, LevelGenerator.Instance) || !Ready())
		{
			indirect.Clear();
			contact.Clear();
			thrown.Clear();
		}
		channel = currentChannel;
		level = LevelGenerator.Instance;
		List<PhysGrabObjectImpactDetector> list = new List<PhysGrabObjectImpactDetector>();
		foreach (KeyValuePair<PhysGrabObjectImpactDetector, ThrownObject> item in thrown)
		{
			if ((Object)(object)item.Key == (Object)null || Time.realtimeSinceStartup > item.Value.Expires)
			{
				list.Add(item.Key);
			}
		}
		foreach (PhysGrabObjectImpactDetector item2 in list)
		{
			thrown.Remove(item2);
		}
	}

	private static bool Ready()
	{
		if ((Object)(object)GameManager.instance != (Object)null && SemiFunc.IsMasterClientOrSingleplayer() && (Object)(object)RunManager.instance != (Object)null && SemiFunc.RunIsLevel() && (Object)(object)LevelGenerator.Instance != (Object)null)
		{
			return LevelGenerator.Instance.Generated;
		}
		return false;
	}

	private static object Read(object value, string field)
	{
		if (value == null)
		{
			return null;
		}
		FieldInfo field2 = value.GetType().GetField(field, Flags);
		if (!(field2 == null))
		{
			return field2.GetValue(value);
		}
		return null;
	}

	private static Enemy EnemyOf(Component component)
	{
		object obj = Read(component, "enemy");
		Enemy val = (Enemy)((obj is Enemy) ? obj : null);
		if ((Object)(object)val != (Object)null)
		{
			return val;
		}
		val = component.GetComponentInParent<Enemy>();
		if ((Object)(object)val != (Object)null)
		{
			return val;
		}
		EnemyParent componentInParent = component.GetComponentInParent<EnemyParent>();
		if (!((Object)(object)componentInParent == (Object)null))
		{
			object obj2 = Read(componentInParent, "Enemy");
			return (Enemy)((obj2 is Enemy) ? obj2 : null);
		}
		return null;
	}

	private static void Attack(Component __instance)
	{
		if (active == null || !Ready())
		{
			return;
		}
		try
		{
			PlayerAvatar val = (PlayerAvatar)((__instance is EnemyUpscreamAnim) ? /*isinst with value type is only supported in some contexts*/: null);
			if (__instance is EnemyUpscreamAnim && (Object)(object)val == (Object)null)
			{
				return;
			}
			if (__instance is EnemyBowtieAnim)
			{
				object obj = Read(Read(__instance, "controller"), "playerTarget");
				val = (PlayerAvatar)((obj is PlayerAvatar) ? obj : null);
			}
			Enemy val2 = EnemyOf(__instance);
			if ((Object)(object)val2 == (Object)null)
			{
				return;
			}
			int num = SemiFunc.EnemyGetIndex(val2);
			double num2 = 10.0;
			int num3 = 0;
			if ((Object)(object)val != (Object)null && EffectWorld.Alive(val))
			{
				active.indirect.Mark(val, num, Time.realtimeSinceStartup, num2);
				num3++;
			}
			foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
			{
				if ((Object)(object)item != (Object)(object)val && EffectWorld.Alive(item) && InSight(val2, item))
				{
					active.indirect.Mark(item, num, Time.realtimeSinceStartup, num2);
					num3++;
				}
			}
			active.log("Attack attribution: event=" + ((object)__instance).GetType().Name + "; enemy=" + num + "; victims=" + num3 + "; thrownTarget=" + ((Object)(object)val != (Object)null) + "; seconds=" + num2);
		}
		catch (Exception ex)
		{
			active.log("Attack attribution failed: " + ex.GetType().Name);
		}
	}

	private static void BeforeColliderCheck(object __instance, out HurtCollider __state)
	{
		__state = checkingCollider;
		object obj = Read(__instance, "<>4__this");
		checkingCollider = (HurtCollider)((obj is HurtCollider) ? obj : null);
	}

	private static void EndColliderCheck(HurtCollider __state)
	{
		checkingCollider = __state;
	}

	private static void MarkBodyHit(HurtCollider collider, PhysGrabObject body)
	{
		if (active == null || !Ready() || (Object)(object)collider == (Object)null || (Object)(object)body == (Object)null)
		{
			return;
		}
		object obj = Read(collider, "enemyHost");
		Enemy val = (Enemy)((obj is Enemy) ? obj : null);
		if ((Object)(object)val == (Object)null)
		{
			val = EnemyOf((Component)(object)collider);
		}
		if (!((Object)(object)val == (Object)null) && (!((Object)(object)((Component)val).GetComponent<EnemySlowWalker>() == (Object)null) || !((Object)(object)((Component)val).GetComponent<EnemyOogly>() == (Object)null)))
		{
			PlayerTumble val2 = ((Component)body).GetComponent<PlayerTumble>();
			if ((Object)(object)val2 == (Object)null)
			{
				val2 = ((Component)body).GetComponentInParent<PlayerTumble>();
			}
			object obj2 = Read(val2, "playerAvatar");
			PlayerAvatar val3 = (PlayerAvatar)((obj2 is PlayerAvatar) ? obj2 : null);
			if (EffectWorld.Alive(val3))
			{
				int num = SemiFunc.EnemyGetIndex(val);
				active.contact.Mark(val3, num, Time.realtimeSinceStartup, 0.5);
				active.log("Attack attribution: event=MonsterBodyHit; enemy=" + num + "; collider=" + ((Object)collider).name);
			}
		}
	}

	private static void BeforeBodyHit(HurtCollider __instance, PhysGrabObject __0, BreakImpact __1)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0002: Invalid comparison between Unknown and I4
		if ((int)__1 <= 0)
		{
			return;
		}
		try
		{
			MarkBodyHit(__instance, __0);
		}
		catch (Exception ex)
		{
			if (active != null)
			{
				active.log("Body attribution failed: " + ex.GetType().Name);
			}
		}
	}

	private static void BeforeBodyDestroy(PhysGrabObjectImpactDetector __instance)
	{
		try
		{
			HurtCollider collider = checkingCollider;
			object obj = Read(__instance, "physGrabObject");
			MarkBodyHit(collider, (PhysGrabObject)((obj is PhysGrabObject) ? obj : null));
		}
		catch (Exception ex)
		{
			if (active != null)
			{
				active.log("Body destruction attribution failed: " + ex.GetType().Name);
			}
		}
	}

	private static void BeforeThrow(EnemyValuableThrower __instance, out PhysGrabObjectImpactDetector __state)
	{
		__state = null;
		if (active != null && Ready() && !((Object)/*isinst with value type is only supported in some contexts*/ == (Object)null))
		{
			object obj = Read(__instance, "valuableTarget");
			PhysGrabObject val = (PhysGrabObject)((obj is PhysGrabObject) ? obj : null);
			if ((Object)(object)val != (Object)null)
			{
				object obj2 = Read(val, "impactDetector");
				__state = (PhysGrabObjectImpactDetector)((obj2 is PhysGrabObjectImpactDetector) ? obj2 : null);
			}
		}
	}

	private static void AfterThrow(EnemyValuableThrower __instance, PhysGrabObjectImpactDetector __state)
	{
		if (active != null && Ready() && !((Object)(object)__state == (Object)null) && !((Object)/*isinst with value type is only supported in some contexts*/ != (Object)null))
		{
			Enemy val = EnemyOf((Component)(object)__instance);
			int num = (((Object)(object)val == (Object)null) ? (-1) : SemiFunc.EnemyGetIndex(val));
			if (num >= 0)
			{
				active.thrown[__state] = new ThrownObject
				{
					Enemy = num,
					Expires = Time.realtimeSinceStartup + 2f
				};
				active.log("Attack attribution: event=RugratThrow; enemy=" + num + "; seconds=2");
			}
		}
	}

	private static void BeforeImpact(PhysGrabObjectImpactDetector __instance, out int __state)
	{
		__state = impactEnemy;
		impactEnemy = -1;
		if (active != null && Ready() && active.thrown.TryGetValue(__instance, out var value) && Time.realtimeSinceStartup <= value.Expires)
		{
			impactEnemy = value.Enemy;
		}
	}

	private static void EndImpact(int __state)
	{
		impactEnemy = __state;
	}

	private static void ObjectDamage(ref int enemyIndex)
	{
		if (active != null && Ready() && enemyIndex < 0 && impactEnemy >= 0)
		{
			enemyIndex = impactEnemy;
			active.log("Attack attribution: event=RugratObjectHit; enemy=" + enemyIndex);
		}
	}

	private static void FloaterSmash(FloaterAttackLogic __instance)
	{
		if (active == null || !Ready() || !(bool)(Read(__instance, "stateStart") ?? ((object)false)))
		{
			return;
		}
		try
		{
			object obj = Read(Read(__instance, "controller"), "enemy");
			Enemy val = (Enemy)((obj is Enemy) ? obj : null);
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			int num = SemiFunc.EnemyGetIndex(val);
			List<PlayerAvatar> list = Read(__instance, "capturedPlayerAvatars") as List<PlayerAvatar>;
			int num2 = 0;
			if (list != null)
			{
				foreach (PlayerAvatar item in list)
				{
					if (EffectWorld.Alive(item) && Read(item, "tumble") != null && (bool)(Read(Read(item, "tumble"), "isTumbling") ?? ((object)false)))
					{
						active.indirect.Mark(item, num, Time.realtimeSinceStartup, 10.0);
						num2++;
					}
				}
			}
			active.log("Attack attribution: event=MentalistSmash; enemy=" + num + "; victims=" + num2 + "; seconds=10");
		}
		catch (Exception ex)
		{
			active.log("Mentalist attribution failed: " + ex.GetType().Name);
		}
	}

	private static bool InSight(Enemy enemy, PlayerAvatar player)
	{
		//IL_003e: 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_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_0055: 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_0057: Unknown result type (might be due to invalid IL or missing references)
		//IL_005c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0104: Unknown result type (might be due to invalid IL or missing references)
		//IL_0107: Unknown result type (might be due to invalid IL or missing references)
		//IL_011e: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ed: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
		//IL_013e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0143: Unknown result type (might be due to invalid IL or missing references)
		object obj = Read(enemy, "Vision");
		EnemyVision val = (EnemyVision)((obj is EnemyVision) ? obj : null);
		if ((Object)(object)val == (Object)null || (Object)(object)val.VisionTransform == (Object)null || (Object)(object)player.PlayerVisionTarget == (Object)null)
		{
			return false;
		}
		Vector3 position = val.VisionTransform.position;
		Vector3 position2 = player.PlayerVisionTarget.VisionTransform.position;
		Vector3 val2 = position2 - position;
		bool flag = (bool)(Read(player, "isCrawling") ?? ((object)false));
		bool flag2 = (bool)(Read(player, "isCrouching") ?? ((object)false));
		float num = ((flag2 || flag) ? val.VisionDistanceCloseCrouch : val.VisionDistanceClose);
		float num2 = (flag ? val.VisionDotCrawl : (flag2 ? val.VisionDotCrouch : val.VisionDotStanding));
		if (((Vector3)(ref val2)).magnitude > val.VisionDistance || (((Vector3)(ref val2)).magnitude > num && Vector3.Dot(val.VisionTransform.forward, ((Vector3)(ref val2)).normalized) < num2))
		{
			return false;
		}
		RaycastHit[] array = Physics.RaycastAll(position, ((Vector3)(ref val2)).normalized, ((Vector3)(ref val2)).magnitude, LayerMask.op_Implicit((LayerMask)Read(enemy, "VisionMask")), (QueryTriggerInteraction)1);
		for (int i = 0; i < array.Length; i++)
		{
			RaycastHit val3 = array[i];
			if (!((RaycastHit)(ref val3)).transform.IsChildOf(((Component)player).transform) && !((RaycastHit)(ref val3)).transform.IsChildOf(((Component)((Component)enemy).GetComponentInParent<EnemyParent>()).transform))
			{
				return false;
			}
		}
		return true;
	}

	private static void BeforeHit(HurtCollider __instance, PlayerAvatar __0, out HitScope __state)
	{
		__state = new HitScope
		{
			Enemy = hurtEnemy,
			Player = hurtPlayer
		};
		hurtEnemy = null;
		hurtPlayer = null;
		if (active == null || !Ready() || !EffectWorld.Alive(__0))
		{
			return;
		}
		try
		{
			object obj = Read(__instance, "enemyHost");
			Enemy val = (Enemy)((obj is Enemy) ? obj : null);
			if ((Object)(object)val == (Object)null)
			{
				val = EnemyOf((Component)(object)__instance);
			}
			if (!((Object)(object)val == (Object)null) && !((Object)(object)((Component)val).GetComponent<EnemyHidden>() != (Object)null))
			{
				hurtEnemy = val;
				hurtPlayer = __0;
				int num = (int)(Read(__instance, "playerDamage") ?? ((object)0));
				if (!(bool)(Read(__instance, "detectionOnly") ?? ((object)false)) && num > 0 && num >= (int)Read(__0.playerHealth, "health"))
				{
					active.contact.Mark(__0, SemiFunc.EnemyGetIndex(val), Time.realtimeSinceStartup, 0.5);
				}
			}
		}
		catch (Exception ex)
		{
			active.log("Direct contact attribution failed: " + ex.GetType().Name);
		}
	}

	private static void EndHit(HitScope __state)
	{
		if (__state != null)
		{
			hurtEnemy = __state.Enemy;
			hurtPlayer = __state.Player;
		}
	}

	private static void DirectDamage(PlayerHealth __instance, ref int enemyIndex)
	{
		if (active != null && Ready() && enemyIndex < 0 && !((Object)(object)hurtEnemy == (Object)null) && !((Object)(object)hurtPlayer == (Object)null) && !((Object)(object)hurtPlayer.playerHealth != (Object)(object)__instance))
		{
			enemyIndex = SemiFunc.EnemyGetIndex(hurtEnemy);
		}
	}

	private static void ClearPlayer(PlayerAvatar __instance)
	{
		if (active != null)
		{
			active.indirect.Remove(__instance);
			active.contact.Remove(__instance);
		}
	}

	internal int Resolve(PlayerAvatar player, int nativeIndex)
	{
		if (!Ready() || !object.ReferenceEquals(level, LevelGenerator.Instance))
		{
			return nativeIndex;
		}
		int num = contact.Take(player, nativeIndex, Time.realtimeSinceStartup);
		int num2 = indirect.Take(player, num, Time.realtimeSinceStartup);
		log("Kill attribution: native=" + nativeIndex + ", resolved=" + num2 + ", mode=" + ((nativeIndex >= 0) ? "vanilla" : ((num >= 0) ? "lethal-contact" : ((num2 >= 0) ? "attack-window" : "unassigned"))));
		return num2;
	}

	public void Dispose()
	{
		if (active == this)
		{
			active = null;
		}
		harmony.UnpatchSelf();
		indirect.Clear();
		contact.Clear();
		thrown.Clear();
	}
}
public sealed class KillCreditWindow<T>
{
	private sealed class Entry
	{
		public int Enemy;

		public double Expires;
	}

	private readonly Dictionary<T, Entry> entries = new Dictionary<T, Entry>();

	public void Mark(T player, int enemy, double now, double seconds)
	{
		if (enemy >= 0 && seconds > 0.0)
		{
			entries[player] = new Entry
			{
				Enemy = enemy,
				Expires = now + seconds
			};
		}
	}

	public int Take(T player, int directEnemy, double now)
	{
		Entry value;
		bool flag = entries.TryGetValue(player, out value);
		entries.Remove(player);
		if (directEnemy >= 0)
		{
			return directEnemy;
		}
		if (!flag || !(now <= value.Expires))
		{
			return -1;
		}
		return value.Enemy;
	}

	public void Remove(T player)
	{
		entries.Remove(player);
	}

	public void Clear()
	{
		entries.Clear();
	}
}
internal sealed class KillHooks : IDisposable
{
	private static readonly FieldInfo Dead = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private static KillHooks current;

	private readonly Harmony harmony = new Harmony("schleimi.repotwitchintegration.kills");

	private readonly Action<int> award;

	private readonly Func<PlayerAvatar, int, int> resolve;

	private readonly Action<string> error;

	public KillHooks(Action<int> award, Action<string> error, Func<PlayerAvatar, int, int> resolve = null)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Expected O, but got Unknown
		//IL_0085: Unknown result type (might be due to invalid IL or missing references)
		//IL_009a: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a7: Expected O, but got Unknown
		//IL_00a7: Expected O, but got Unknown
		this.award = award;
		this.error = error;
		this.resolve = resolve;
		if (Dead == null)
		{
			throw new InvalidOperationException("Player death tracking is not supported by this game version.");
		}
		MethodInfo methodInfo = AccessTools.Method(typeof(PlayerAvatar), "PlayerDeathRPC", (Type[])null, (Type[])null);
		if (methodInfo == null)
		{
			throw new InvalidOperationException("Player death event was not found.");
		}
		harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(KillHooks), "BeforeDeath", (Type[])null), new HarmonyMethod(typeof(KillHooks), "AfterDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		current = this;
	}

	private static void BeforeDeath(PlayerAvatar __instance, out bool __state)
	{
		__state = (bool)Dead.GetValue(__instance);
	}

	private static void AfterDeath(PlayerAvatar __instance, int enemyIndex, bool __state)
	{
		if (current == null || __state || !(bool)Dead.GetValue(__instance))
		{
			return;
		}
		try
		{
			current.award((current.resolve == null) ? enemyIndex : current.resolve(__instance, enemyIndex));
		}
		catch (Exception ex)
		{
			current.error("Viewer kill tracking failed: " + ex.GetType().Name);
		}
	}

	public void Dispose()
	{
		if (current == this)
		{
			current = null;
		}
		harmony.UnpatchSelf();
	}
}
public sealed class ViewerScore
{
	public string UserId;

	public string Login;

	public long Kills;
}
public sealed class KillRanking
{
	private Dictionary<string, Dictionary<string, ViewerScore>> channels = new Dictionary<string, Dictionary<string, ViewerScore>>();

	private readonly string path;

	private readonly Action<string> error;

	public bool Persistent { get; private set; }

	public long Revision { get; private set; }

	public KillRanking(string path, bool persistent, Action<string> error)
	{
		this.path = path;
		this.error = error;
		Persistent = persistent;
		if (!persistent || !File.Exists(path))
		{
			return;
		}
		try
		{
			Dictionary<string, Dictionary<string, ViewerScore>> dictionary = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, ViewerScore>>>(File.ReadAllText(path));
			if (dictionary == null)
			{
				return;
			}
			foreach (KeyValuePair<string, Dictionary<string, ViewerScore>> item in dictionary)
			{
				if (!ValidId(item.Key) || item.Value == null)
				{
					continue;
				}
				Dictionary<string, ViewerScore> dictionary2 = new Dictionary<string, ViewerScore>();
				foreach (KeyValuePair<string, ViewerScore> item2 in item.Value)
				{
					if (ValidId(item2.Key) && item2.Value != null && ValidLogin(item2.Value.Login) && item2.Value.Kills >= 1)
					{
						dictionary2[item2.Key] = new ViewerScore
						{
							UserId = item2.Key,
							Login = item2.Value.Login,
							Kills = item2.Value.Kills
						};
					}
				}
				channels[item.Key] = dictionary2;
			}
		}
		catch (Exception e)
		{
			Report(e);
		}
	}

	private static bool ValidId(string value)
	{
		if (!string.IsNullOrEmpty(value))
		{
			return value.All((char c) => c >= '0' && c <= '9');
		}
		return false;
	}

	private static bool ValidLogin(string value)
	{
		if (!string.IsNullOrEmpty(value) && value.Length <= 25)
		{
			return value.All((char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_');
		}
		return false;
	}

	public long Award(string channel, string userId, string login)
	{
		if (!ValidId(channel) || !ValidId(userId) || !ValidLogin(login))
		{
			throw new ArgumentException("Invalid viewer identity.");
		}
		if (!channels.TryGetValue(channel, out var value))
		{
			value = new Dictionary<string, ViewerScore>();
			channels[channel] = value;
		}
		if (!value.TryGetValue(userId, out var value2))
		{
			ViewerScore viewerScore = new ViewerScore();
			viewerScore.UserId = userId;
			value2 = (value[userId] = viewerScore);
		}
		value2.Login = login;
		checked
		{
			value2.Kills++;
		}
		Revision++;
		Save();
		return value2.Kills;
	}

	public void UpdateLogin(string channel, string userId, string login)
	{
		if (channel != null && userId != null && ValidLogin(login) && channels.TryGetValue(channel, out var value) && value.TryGetValue(userId, out var value2) && !(value2.Login == login))
		{
			value2.Login = login;
			Revision++;
			Save();
		}
	}

	public string LatestLogin(string channel, string userId, string fallback)
	{
		if (!channels.TryGetValue(channel, out var value) || !value.TryGetValue(userId, out var value2))
		{
			return fallback;
		}
		return value2.Login;
	}

	public ViewerScore[] TopFive(string channel)
	{
		if (channel == null || !channels.TryGetValue(channel, out var value))
		{
			return new ViewerScore[0];
		}
		return (from s in value.Values.OrderByDescending((ViewerScore s) => s.Kills).ThenBy<ViewerScore, string>((ViewerScore s) => s.Login, StringComparer.OrdinalIgnoreCase).ThenBy<ViewerScore, string>((ViewerScore s) => s.UserId, StringComparer.Ordinal)
				.Take(5)
			select new ViewerScore
			{
				UserId = s.UserId,
				Login = s.Login,
				Kills = s.Kills
			}).ToArray();
	}

	public void SetPersistent(bool persistent)
	{
		Persistent = persistent;
		Save();
	}

	public void ResetAll()
	{
		channels.Clear();
		Revision++;
		Save();
	}

	public void LostRun()
	{
		if (!Persistent)
		{
			channels.Clear();
			Revision++;
			Save();
		}
	}

	private void Save()
	{
		try
		{
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			string sourceFileName = path + ".tmp";
			File.WriteAllText(sourceFileName, JsonConvert.SerializeObject((object)channels, (Formatting)1));
			if (File.Exists(path))
			{
				File.Replace(sourceFileName, path, null, ignoreMetadataErrors: true);
			}
			else
			{
				File.Move(sourceFileName, path);
			}
		}
		catch (Exception e)
		{
			Report(e);
		}
	}

	private void Report(Exception e)
	{
		if (error != null)
		{
			error("Could not load/save the viewer ranking: " + e.GetType().Name);
		}
	}
}
public sealed class MonsterDefinition
{
	public readonly string Name;

	public readonly string Command;

	public readonly string Alias;

	public readonly bool Group;

	public MonsterDefinition(string name, string command, string alias, bool group)
	{
		Name = name;
		Command = command;
		Alias = alias;
		Group = group;
	}
}
public static class MonsterCatalog
{
	public static readonly MonsterDefinition[] All = new MonsterDefinition[29]
	{
		new MonsterDefinition("Apex Predator", "!duck", "!apex", group: false),
		new MonsterDefinition("Animal", "!animal", null, group: false),
		new MonsterDefinition("Banger", "!banger", "!skull", group: true),
		new MonsterDefinition("Bella", "!bella", null, group: false),
		new MonsterDefinition("Birthday Boy", "!birthdayboy", "!birthday", group: false),
		new MonsterDefinition("Bowtie", "!bowtie", null, group: false),
		new MonsterDefinition("Chef", "!chef", null, group: false),
		new MonsterDefinition("Cleanup Crew", "!cleanupcrew", "!cleanup", group: false),
		new MonsterDefinition("Clown", "!clown", "!beamer", group: false),
		new MonsterDefinition("Elsa", "!elsa", null, group: false),
		new MonsterDefinition("Gambit", "!gambit", null, group: false),
		new MonsterDefinition("Gnome", "!gnome", null, group: true),
		new MonsterDefinition("Headgrab", "!headgrab", null, group: false),
		new MonsterDefinition("Headman", "!headman", null, group: false),
		new MonsterDefinition("Heart Hugger", "!hearthugger", "!heart", group: false),
		new MonsterDefinition("Hidden", "!hidden", null, group: false),
		new MonsterDefinition("Huntsman", "!huntsman", "!hunter", group: false),
		new MonsterDefinition("Loom", "!loom", null, group: false),
		new MonsterDefinition("Mentalist", "!mentalist", null, group: false),
		new MonsterDefinition("Oogly", "!oogly", null, group: false),
		new MonsterDefinition("Peeper", "!peeper", "!eye", group: false),
		new MonsterDefinition("Reaper", "!reaper", null, group: false),
		new MonsterDefinition("Robe", "!robe", null, group: false),
		new MonsterDefinition("Rugrat", "!rugrat", "!baby", group: false),
		new MonsterDefinition("Shadow Child", "!shadowchild", "!shadow", group: false),
		new MonsterDefinition("Spewer", "!spewer", null, group: false),
		new MonsterDefinition("Tick", "!tick", null, group: false),
		new MonsterDefinition("Trudge", "!trudge", "!slowwalker", group: false),
		new MonsterDefinition("Upscream", "!upscream", null, group: false)
	};

	public static bool IsRandom(string text)
	{
		return string.Equals((text ?? "").Trim(), "!spawn", StringComparison.OrdinalIgnoreCase);
	}

	public static bool IsList(string text)
	{
		return string.Equals((text ?? "").Trim(), "!monsters", StringComparison.OrdinalIgnoreCase);
	}

	public static MonsterDefinition FindCommand(string text)
	{
		string a = (text ?? "").Trim();
		MonsterDefinition[] all = All;
		foreach (MonsterDefinition monsterDefinition in all)
		{
			if (string.Equals(a, monsterDefinition.Command, StringComparison.OrdinalIgnoreCase) || (monsterDefinition.Alias != null && string.Equals(a, monsterDefinition.Alias, StringComparison.OrdinalIgnoreCase)))
			{
				return monsterDefinition;
			}
		}
		return null;
	}

	public static MonsterDefinition FindName(string name)
	{
		MonsterDefinition[] all = All;
		foreach (MonsterDefinition monsterDefinition in all)
		{
			if (string.Equals((name ?? "").Trim(), monsterDefinition.Name, StringComparison.OrdinalIgnoreCase))
			{
				return monsterDefinition;
			}
		}
		return null;
	}
}
public sealed class SpawnGroup
{
	private readonly byte[] states;

	public int Pending { get; private set; }

	public int Live { get; private set; }

	public int SpawnedCount { get; private set; }

	public long CreditedHp { get; private set; }

	public bool Finished
	{
		get
		{
			if (Pending == 0)
			{
				return Live == 0;
			}
			return false;
		}
	}

	public SpawnGroup(int count)
	{
		if (count < 1)
		{
			throw new ArgumentOutOfRangeException("count");
		}
		states = new byte[count];
		Pending = count;
	}

	public bool Confirm(int member, int maxHp)
	{
		if (states[member] != 0 || maxHp < 1)
		{
			return false;
		}
		states[member] = 1;
		Pending--;
		Live++;
		SpawnedCount++;
		CreditedHp += maxHp;
		return true;
	}

	public bool End(int member)
	{
		if (states[member] == 2)
		{
			return false;
		}
		if (states[member] == 0)
		{
			Pending--;
		}
		else
		{
			Live--;
		}
		states[member] = 2;
		return true;
	}
}
public sealed class MonsterClaim
{
	public readonly string UserId;

	public readonly string Login;

	public bool Active { get; internal set; }

	internal MonsterClaim(string userId, string login)
	{
		UserId = userId;
		Login = login;
	}
}
public sealed class MonsterClaims
{
	private readonly Dictionary<string, MonsterClaim> owners = new Dictionary<string, MonsterClaim>();

	private readonly HashSet<string> seen = new HashSet<string>();

	private readonly Queue<string> seenOrder = new Queue<string>();

	public int Count => owners.Count;

	public static bool IsDuckCommand(string text)
	{
		string a = (text ?? "").Trim();
		if (!string.Equals(a, "!duck", StringComparison.OrdinalIgnoreCase))
		{
			return string.Equals(a, "!apex", StringComparison.OrdinalIgnoreCase);
		}
		return true;
	}

	public bool AcceptMessage(string messageId)
	{
		if (string.IsNullOrEmpty(messageId) || !seen.Add(messageId))
		{
			return false;
		}
		seenOrder.Enqueue(messageId);
		if (seenOrder.Count > 4096)
		{
			seen.Remove(seenOrder.Dequeue());
		}
		return true;
	}

	public MonsterClaim Reserve(string userId, string login)
	{
		if (string.IsNullOrEmpty(userId) || owners.ContainsKey(userId))
		{
			return null;
		}
		MonsterClaim monsterClaim = new MonsterClaim(userId, login);
		owners.Add(userId, monsterClaim);
		return monsterClaim;
	}

	public bool Activate(MonsterClaim claim)
	{
		if (claim == null || !owners.TryGetValue(claim.UserId, out var value) || value != claim)
		{
			return false;
		}
		claim.Active = true;
		return true;
	}

	public bool Release(MonsterClaim claim)
	{
		if (claim == null || !owners.TryGetValue(claim.UserId, out var value) || value != claim)
		{
			return false;
		}
		owners.Remove(claim.UserId);
		claim.Active = false;
		return true;
	}

	public void ResetLevel()
	{
		foreach (MonsterClaim value in owners.Values)
		{
			value.Active = false;
		}
		owners.Clear();
	}
}
internal sealed class MonsterSpawns
{
	private sealed class Plan
	{
		public MonsterDefinition Monster;

		public readonly List<PrefabRef> Members = new List<PrefabRef>();

		public readonly List<PrefabRef> Helpers = new List<PrefabRef>();
	}

	private sealed class Batch
	{
		public MonsterClaim Claim;

		public MonsterDefinition Monster;

		public SpawnGroup State;

		public string Channel;

		public int Expected;

		public bool Announced;
	}

	private sealed class Slot
	{
		public EnemyParent Parent;

		public string Path;

		public Batch Batch;

		public Batch Owner;

		public int Member;

		public bool Active;

		public bool Reusable;

		public bool EverSpawned;

		public double Deadline;

		public double ReuseAfter;

		public Renderer[] NameRenderers;
	}

	private static readonly BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	private static readonly FieldInfo SetupDone = typeof(EnemyParent).GetField("SetupDone", Fields);

	private static readonly FieldInfo Spawned = typeof(EnemyParent).GetField("Spawned", Fields);

	private static readonly FieldInfo FirstPointUsed = typeof(EnemyParent).GetField("firstSpawnPointUsed", Fields);

	private static readonly FieldInfo AllDead = typeof(RunManager).GetField("allPlayersDead", Fields);

	private static readonly FieldInfo ChangingLevel = typeof(RunManager).GetField("waitToChangeScene", Fields);

	private readonly MonsterClaims claims = new MonsterClaims();

	private readonly List<Slot> slots = new List<Slot>();

	private readonly List<Batch> batches = new List<Batch>();

	private readonly Dictionary<MonsterDefinition, Plan> plans = new Dictionary<MonsterDefinition, Plan>();

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

	private readonly Action<string> reply;

	private readonly ManualLogSource log;

	private readonly ChatPoints points;

	private readonly KillRanking ranking;

	private readonly Random random = new Random();

	private LevelGenerator level;

	private bool lostAuthority;

	private bool catalogLoaded;

	private bool runLost;

	private readonly RunLossNotice lossNotice = new RunLossNotice();

	private double nextPointsReply;

	private double nextSpawnHelp;

	private GUIStyle nameStyle;

	public string Channel { get; set; }

	public string Status { get; private set; }

	public int ActiveCount => claims.Count;

	public int ActiveMonsterCount
	{
		get
		{
			int num = 0;
			foreach (Slot slot in slots)
			{
				if ((Object)(object)slot.Parent != (Object)null && slot.Active && slot.Batch != null)
				{
					num++;
				}
			}
			return num;
		}
	}

	public int AvailableCount => plans.Count;

	public void DrawViewerNames()
	{
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Invalid comparison between Unknown and I4
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Expected O, but got Unknown
		//IL_0111: Unknown result type (might be due to invalid IL or missing references)
		//IL_01a0: 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_01c1: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_01cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_0200: Unknown result type (might be due to invalid IL or missing references)
		//IL_020c: Unknown result type (might be due to invalid IL or missing references)
		//IL_021e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0228: Unknown result type (might be due to invalid IL or missing references)
		//IL_022d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0232: Unknown result type (might be due to invalid IL or missing references)
		//IL_017a: Unknown result type (might be due to invalid IL or missing references)
		//IL_016e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0173: Unknown result type (might be due to invalid IL or missing references)
		//IL_02ed: Unknown result type (might be due to invalid IL or missing references)
		//IL_02f4: Expected O, but got Unknown
		//IL_02fc: Unknown result type (might be due to invalid IL or missing references)
		//IL_0301: Unknown result type (might be due to invalid IL or missing references)
		//IL_0355: Unknown result type (might be due to invalid IL or missing references)
		//IL_036c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0376: Unknown result type (might be due to invalid IL or missing references)
		if ((int)Event.current.type != 7)
		{
			return;
		}
		Camera main = Camera.main;
		if ((Object)(object)main == (Object)null || !((Behaviour)main).enabled)
		{
			return;
		}
		if (nameStyle == null)
		{
			GUIStyle val = new GUIStyle(GUI.skin.label);
			val.alignment = (TextAnchor)4;
			val.richText = false;
			val.fontStyle = (FontStyle)1;
			nameStyle = val;
		}
		nameStyle.fontSize = Mathf.Clamp(Mathf.RoundToInt((float)Screen.height / 60f), 14, 32);
		RaycastHit val4 = default(RaycastHit);
		Vector3 val6 = default(Vector3);
		Rect val9 = default(Rect);
		foreach (Slot slot in slots)
		{
			if ((Object)(object)slot.Parent == (Object)null || !slot.Active || slot.Batch == null || slot.Batch.Channel != Channel || !((Component)slot.Parent).gameObject.activeInHierarchy)
			{
				continue;
			}
			if (slot.NameRenderers == null)
			{
				slot.NameRenderers = ((Component)slot.Parent).GetComponentsInChildren<Renderer>(true);
			}
			Bounds val2 = default(Bounds);
			bool flag = false;
			Renderer[] nameRenderers = slot.NameRenderers;
			foreach (Renderer val3 in nameRenderers)
			{
				if (!((Object)(object)val3 == (Object)null) && (val3 is MeshRenderer || val3 is SkinnedMeshRenderer) && val3.enabled && ((Component)val3).gameObject.activeInHierarchy && val3.isVisible)
				{
					if (!flag)
					{
						val2 = val3.bounds;
					}
					else
					{
						((Bounds)(ref val2)).Encapsulate(val3.bounds);
					}
					flag = true;
				}
			}
			if (flag && !(Vector3.Distance(((Component)main).transform.position, ((Bounds)(ref val2)).center) > 40f) && (!Physics.Linecast(((Component)main).transform.position, ((Bounds)(ref val2)).center, ref val4, LayerMask.op_Implicit(SemiFunc.LayerMaskGetVisionObstruct()), (QueryTriggerInteraction)1) || ((RaycastHit)(ref val4)).transform.IsChildOf(((Component)slot.Parent).transform)))
			{
				Vector3 val5 = main.WorldToViewportPoint(new Vector3(((Bounds)(ref val2)).center.x, ((Bounds)(ref val2)).max.y + 0.2f, ((Bounds)(ref val2)).center.z));
				if (!(val5.z <= 0f) && !(val5.x < 0f) && !(val5.x > 1f) && !(val5.y < 0f) && !(val5.y > 1f))
				{
					((Vector3)(ref val6))..ctor(val5.x * (float)Screen.width, val5.y * (float)Screen.height, val5.z);
					string text = ranking.LatestLogin(slot.Batch.Channel, slot.Batch.Claim.UserId, slot.Batch.Claim.Login);
					GUIContent val7 = new GUIContent(text);
					Vector2 val8 = nameStyle.CalcSize(val7);
					((Rect)(ref val9))..ctor(val6.x - val8.x / 2f - 8f, (float)Screen.height - val6.y - val8.y, val8.x + 16f, val8.y + 4f);
					GUI.Box(val9, GUIContent.none);
					nameStyle.normal.textColor = Color.white;
					GUI.Label(val9, val7, nameStyle);
				}
			}
		}
	}

	public MonsterSpawns(Action<string> reply, ManualLogSource log, ChatPoints points, KillRanking ranking)
	{
		this.reply = reply;
		this.log = log;
		this.points = points;
		this.ranking = ranking;
		Status = "29 monster commands and !spawn ready.";
	}

	public bool Tick(double now)
	{
		bool flag = (Object)(object)GameManager.instance != (Object)null && SemiFunc.IsMasterClientOrSingleplayer();
		bool flag2 = (Object)(object)RunManager.instance != (Object)null && AllDead != null && (bool)AllDead.GetValue(RunManager.instance);
		bool arena = (Object)(object)RunManager.instance != (Object)null && SemiFunc.RunIsArena();
		if (flag)
		{
			lossNotice.ObserveMission(LevelGenerator.Instance, !flag2 && (Object)(object)RunManager.instance != (Object)null && SemiFunc.RunIsLevel() && (Object)(object)LevelGenerator.Instance != (Object)null && LevelGenerator.Instance.Generated);
		}
		if (flag && flag2 && !runLost)
		{
			points.LostRun();
			ranking.LostRun();
			if (lossNotice.TryAnnounce(arena))
			{
				if (!points.Persistent)
				{
					Reply(Channel, "[REPO] Run lost: shared chat points have been reset.");
				}
				if (!ranking.Persistent)
				{
					Reply(Channel, "[REPO] Run lost: the viewer kill ranking has been reset.");
				}
			}
		}
		runLost = flag2;
		LevelGenerator instance = LevelGenerator.Instance;
		if (!object.ReferenceEquals(level, instance))
		{
			foreach (Slot slot2 in slots)
			{
				if ((Object)(object)slot2.Parent != (Object)null)
				{
					((MonoBehaviour)slot2.Parent).StopAllCoroutines();
				}
			}
			slots.Clear();
			batches.Clear();
			claims.ResetLevel();
			rejectionTimes.Clear();
			plans.Clear();
			level = instance;
			catalogLoaded = false;
			lostAuthority = false;
			Status = "Level changed: viewer monster slots have been reset.";
			return true;
		}
		if ((Object)(object)level == (Object)null)
		{
			return false;
		}
		if (!flag)
		{
			if (slots.Count > 0)
			{
				foreach (Slot slot3 in slots)
				{
					if ((Object)(object)slot3.Parent != (Object)null)
					{
						((MonoBehaviour)slot3.Parent).StopAllCoroutines();
					}
				}
				lostAuthority = true;
			}
			return false;
		}
		if (!catalogLoaded && level.Generated && (Object)(object)EnemyDirector.instance != (Object)null)
		{
			LoadCatalog();
		}
		for (int num = slots.Count - 1; num >= 0; num--)
		{
			Slot slot = slots[num];
			if ((Object)(object)slot.Parent == (Object)null)
			{
				EndMember(slot, now, reusable: false);
				slots.RemoveAt(num);
			}
			else if (slot.Batch != null)
			{
				bool flag3 = (bool)Spawned.GetValue(slot.Parent);
				bool flag4 = (bool)FirstPointUsed.GetValue(slot.Parent);
				if (!slot.Active)
				{
					if (flag2)
					{
						Retire(slot);
						EndMember(slot, now, reusable: false);
					}
					else if (flag3 && flag4)
					{
						EnemyHealth componentInChildren = ((Component)slot.Parent).GetComponentInChildren<EnemyHealth>(true);
						if ((Object)(object)componentInChildren == (Object)null || componentInChildren.health <= 0)
						{
							Retire(slot);
							EndMember(slot, now, reusable: false);
						}
						else if (slot.Batch.State.Confirm(slot.Member, componentInChildren.health))
						{
							slot.Active = true;
							slot.EverSpawned = true;
							claims.Activate(slot.Batch.Claim);
							points.Credit(slot.Batch.Channel, componentInChildren.health);
							log.LogInfo((object)("Chat monster active: " + slot.Batch.Monster.Name + "; user=" + slot.Batch.Claim.UserId + "; HP=" + componentInChildren.health));
						}
					}
					else if (now >= slot.Deadline)
					{
						Retire(slot);
						EndMember(slot, now, reusable: false);
					}
				}
				else if (!flag3)
				{
					Retire(slot);
					EndMember(slot, now, reusable: true);
				}
			}
		}
		for (int num2 = batches.Count - 1; num2 >= 0; num2--)
		{
			Batch batch = batches[num2];
			if (!batch.Announced && batch.State.Pending == 0)
			{
				batch.Announced = true;
				Status = batch.Claim.Login + ": " + batch.Monster.Name + " " + batch.State.SpawnedCount + "/" + batch.Expected + " spawned.";
				Reply(batch.Channel, "[REPO] @" + batch.Claim.Login + " " + batch.Monster.Name + ": " + batch.State.SpawnedCount + "/" + batch.Expected + " spawned. +" + ChatPoints.Format(batch.State.CreditedHp) + " Chat Points | Pool: " + points.Display(batch.Channel));
			}
			if (batch.State.Finished)
			{
				claims.Release(batch.Claim);
				Reply(batch.Channel, "[REPO] @" + batch.Claim.Login + " Your monster slot is available again. All members are gone or could not spawn.");
				batches.RemoveAt(num2);
			}
		}
		return false;
	}

	public void Handle(ChatMessage message, double now)
	{
		//IL_02d9: Unknown result type (might be due to invalid IL or missing references)
		//IL_031c: Unknown result type (might be due to invalid IL or missing references)
		bool flag = MonsterCatalog.IsRandom(message.Text);
		bool flag2 = MonsterCatalog.IsList(message.Text);
		bool flag3 = string.Equals((message.Text ?? "").Trim(), "!points", StringComparison.OrdinalIgnoreCase);
		MonsterDefinition monsterDefinition = MonsterCatalog.FindCommand(message.Text);
		if ((!flag && !flag2 && !flag3 && monsterDefinition == null) || !claims.AcceptMessage(message.Id))
		{
			return;
		}
		string text = SafeLogin(message.Login);
		if (string.IsNullOrEmpty(Channel) || string.IsNullOrEmpty(message.UserId) || text == null || (message.ReceivedAtUtc != default(DateTime) && DateTime.UtcNow - message.ReceivedAtUtc > TimeSpan.FromSeconds(10.0)))
		{
			return;
		}
		if (flag3)
		{
			if (now >= nextPointsReply)
			{
				nextPointsReply = now + 10.0;
				Reply(Channel, "[REPO] Shared Chat Points: " + points.Display(Channel) + " points.");
			}
			return;
		}
		if (flag2)
		{
			if (now >= nextSpawnHelp)
			{
				nextSpawnHelp = now + 30.0;
				Reply(Channel, UpgradeCommands.SpawnHelp());
			}
			return;
		}
		string text2 = UnavailableReason();
		if (text2 != null)
		{
			Reject(message.UserId, text, text2, now);
			return;
		}
		if (!catalogLoaded)
		{
			LoadCatalog();
		}
		Plan value = null;
		if (flag && plans.Count > 0)
		{
			List<Plan> list = new List<Plan>(plans.Values);
			value = list[random.Next(list.Count)];
		}
		else if (monsterDefinition != null)
		{
			plans.TryGetValue(monsterDefinition, out value);
		}
		if (value == null)
		{
			Reject(message.UserId, text, "This monster is not available in the installed game version.", now);
			return;
		}
		MonsterClaim monsterClaim = claims.Reserve(message.UserId, text);
		if (monsterClaim == null)
		{
			Reject(message.UserId, text, "You already have an active or pending chat monster / group.", now);
			return;
		}
		Batch batch = new Batch();
		batch.Claim = monsterClaim;
		batch.Monster = value.Monster;
		batch.Channel = Channel;
		batch.Expected = value.Members.Count;
		batch.State = new SpawnGroup(value.Members.Count);
		Batch batch2 = batch;
		try
		{
			LevelPoint val = FindInitialPoint();
			if ((Object)(object)val == (Object)null)
			{
				throw new InvalidOperationException("No Vanilla spawn point is available outside the truck.");
			}
			foreach (PrefabRef helper in value.Helpers)
			{
				if (!(((Object)(object)((PrefabRef<GameObject>)(object)helper).Prefab.GetComponent<EnemyGnomeDirector>() != (Object)null) ? ((Object)(object)EnemyGnomeDirector.instance != (Object)null) : ((Object)(object)EnemyBangDirector.instance != (Object)null)))
				{
					Create(helper, ((Component)val).transform.position);
				}
			}
			for (int i = 0; i < value.Members.Count; i++)
			{
				SpawnMember(value.Members[i], batch2, i, ((Component)val).transform.position, now);
			}
			batches.Add(batch2);
			Status = value.Monster.Name + " for " + text + " is being prepared (" + value.Members.Count + ") ...";
		}
		catch (Exception ex)
		{
			foreach (Slot slot in slots)
			{
				if (slot.Batch == batch2)
				{
					Retire(slot);
					EndMember(slot, now, reusable: false);
				}
			}
			claims.Release(monsterClaim);
			Status = "Monster spawn failed. See the BepInEx log for details.";
			log.LogError((object)("Chat monster spawn failed: " + ex));
			Reject(message.UserId, text, "Spawn failed. No points added; your monster slot remains available.", now);
		}
	}

	private void SpawnMember(PrefabRef prefab, Batch batch, int member, Vector3 position, double now)
	{
		//IL_014f: Unknown result type (might be due to invalid IL or missing references)
		//IL_01bf: Unknown result type (might be due to invalid IL or missing references)
		Slot slot = null;
		foreach (Slot slot3 in slots)
		{
			if (slot3.Path == ((PrefabRef<GameObject>)(object)prefab).ResourcePath && (Object)(object)slot3.Parent != (Object)null && slot3.Batch == null && slot3.Reusable && now >= slot3.ReuseAfter && slot3.Owner != null && slot3.Owner.Channel == batch.Channel && slot3.Owner.Claim.UserId == batch.Claim.UserId)
			{
				slot = slot3;
				break;
			}
		}
		if (slot != null)
		{
			slot.Batch = batch;
			slot.Owner = batch;
			slot.Member = member;
			slot.Active = false;
			slot.Reusable = false;
			slot.Deadline = now + 25.0;
			((Component)slot.Parent).GetComponentInChildren<EnemyHealth>(true).OnSpawn();
			FirstPointUsed.SetValue(slot.Parent, false);
			EnemyDirector.instance.FirstSpawnPointAdd(slot.Parent);
			((MonoBehaviour)slot.Parent).StartCoroutine("Logic");
			((MonoBehaviour)slot.Parent).StartCoroutine("PlayerCloseLogic");
		}
		else
		{
			GameObject val = Create(prefab, position);
			EnemyParent component = val.GetComponent<EnemyParent>();
			Enemy componentInChildren = val.GetComponentInChildren<Enemy>(true);
			Slot slot2 = new Slot();
			slot2.Parent = component;
			slot2.Path = ((PrefabRef<GameObject>)(object)prefab).ResourcePath;
			slot2.Batch = batch;
			slot2.Owner = batch;
			slot2.Member = member;
			slot2.Deadline = now + 25.0;
			slot = slot2;
			slots.Add(slot);
			componentInChildren.EnemyTeleported(position);
			EnemyDirector.instance.FirstSpawnPointAdd(component);
			SetupDone.SetValue(component, true);
		}
	}

	private static GameObject Create(PrefabRef prefab, Vector3 position)
	{
		//IL_0020: 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_000d: Unknown result type (might be due to invalid IL or missing references)
		//IL_000e: Unknown result type (might be due to invalid IL or missing references)
		GameObject val = (GameManager.Multiplayer() ? PhotonNetwork.InstantiateRoomObject(((PrefabRef<GameObject>)(object)prefab).ResourcePath, position, Quaternion.identity, (byte)0, (object[])null) : Object.Instantiate<GameObject>(((PrefabRef<GameObject>)(object)prefab).Prefab, position, Quaternion.identity));
		if ((Object)(object)val == (Object)null)
		{
			throw new InvalidOperationException("Could not create Vanilla prefab: " + ((PrefabRef<GameObject>)(object)prefab).ResourcePath);
		}
		return val;
	}

	public void RecordKill(int enemyIndex)
	{
		if (enemyIndex < 0 || lostAuthority || runLost || (Object)(object)GameManager.instance == (Object)null || !SemiFunc.IsMasterClientOrSingleplayer() || (Object)(object)RunManager.instance == (Object)null || !SemiFunc.RunIsLevel() || (Object)(object)level == (Object)null || !object.ReferenceEquals(level, LevelGenerator.Instance) || (Object)(object)EnemyDirector.instance == (Object)null || enemyIndex >= EnemyDirector.instance.enemiesSpawned.Count)
		{
			return;
		}
		EnemyParent val = EnemyDirector.instance.enemiesSpawned[enemyIndex];
		if ((Object)(object)val == (Object)null)
		{
			return;
		}
		foreach (Slot slot in slots)
		{
			if (!((Object)(object)slot.Parent != (Object)(object)val) && slot.Owner != null)
			{
				if (slot.EverSpawned || ((bool)FirstPointUsed.GetValue(val) && (bool)Spawned.GetValue(val)))
				{
					Batch owner = slot.Owner;
					string text = ranking.LatestLogin(owner.Channel, owner.Claim.UserId, owner.Claim.Login);
					long num = ranking.Award(owner.Channel, owner.Claim.UserId, text);
					Reply(owner.Channel, "[REPO] @" + text + " +1 kill! Your " + owner.Monster.Name + " killed a player. Total: " + num + " kills.");
					log.LogInfo((object)("Viewer kill: user=" + owner.Claim.UserId + "; monster=" + owner.Monster.Name + "; total=" + num));
				}
				break;
			}
		}
	}

	private string Unavai