Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RepoTwitchIntegration by Schleimi v1.2.9
BepInEx/plugins/RepoTwitchIntegration/RepoTwitchIntegration.dll
Decompiled 17 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.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