Decompiled source of CardShopCoopCommunity v2.0.6
BepInEx/plugins/CardShopCoop/CardShopCoop.dll
Decompiled 13 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.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using CC; using CMF; using CardShopCoop.Api; using CardShopCoop.Attributes; using CardShopCoop.Modules.Catalog; using CardShopCoop.Modules.DebugTools; using CardShopCoop.Modules.Decoration; using CardShopCoop.Modules.Economy; using CardShopCoop.Modules.Grading; using CardShopCoop.Modules.Hud; using CardShopCoop.Modules.Npc; using CardShopCoop.Modules.PlayTable; using CardShopCoop.Modules.Prediction; using CardShopCoop.Modules.Presence; using CardShopCoop.Modules.Pricing; using CardShopCoop.Modules.Register; using CardShopCoop.Modules.Report; using CardShopCoop.Modules.SaveTransfer; using CardShopCoop.Modules.SessionInput; using CardShopCoop.Modules.World; using CardShopCoop.Net; using CardShopCoop.Net.Connection; using CardShopCoop.Net.Datagrams; using CardShopCoop.Net.Kcp; using CardShopCoop.Net.Messages; using CardShopCoop.Net.Protocol; using CardShopCoop.Runtime; using CardShopCoop.UI; using CardShopCoop.Util; using HarmonyLib; using I2.Loc; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using Steamworks; using TMPro; using Unity.Profiling; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using UnityEngine.UI; using kcp2k; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("ConnectionState")] [assembly: InternalsVisibleTo("ProtocolTransport")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Devin 'Meepen' Korb")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("CardShopCoopCommunity for TCG Card Shop Simulator.")] [assembly: AssemblyFileVersion("2.0.6.0")] [assembly: AssemblyInformationalVersion("2.0.6+7377084dcc6c7363bdd27c3bdca151eaad5dfb45")] [assembly: AssemblyProduct("CardShopCoopCommunity")] [assembly: AssemblyTitle("CardShopCoop")] [assembly: AssemblyVersion("2.0.6.0")] namespace kcp2k { internal struct AckItem { internal uint serialNumber; internal uint timestamp; } public class Kcp { public const int RTO_NDL = 30; public const int RTO_MIN = 100; public const int RTO_DEF = 200; public const int RTO_MAX = 60000; public const int CMD_PUSH = 81; public const int CMD_ACK = 82; public const int CMD_WASK = 83; public const int CMD_WINS = 84; public const int ASK_SEND = 1; public const int ASK_TELL = 2; public const int WND_SND = 32; public const int WND_RCV = 128; public const int MTU_DEF = 1200; public const int MTU_MIN = 50; public const int ACK_FAST = 3; public const int INTERVAL = 100; public const int OVERHEAD = 24; public const int FRG_MAX = 255; public const int DEADLINK = 20; public const int THRESH_INIT = 2; public const int THRESH_MIN = 2; public const int PROBE_INIT = 7000; public const int PROBE_LIMIT = 120000; public const int FASTACK_LIMIT = 5; internal int state; private readonly uint conv; internal uint mtu; internal uint mss; internal uint snd_una; internal uint snd_nxt; internal uint rcv_nxt; internal uint ssthresh; internal int rx_rttval; internal int rx_srtt; internal int rx_rto; internal int rx_minrto; internal uint snd_wnd; internal uint rcv_wnd; internal uint rmt_wnd; internal uint cwnd; internal uint probe; internal uint interval; internal uint ts_flush; internal uint xmit; internal uint nodelay; internal bool updated; internal uint ts_probe; internal uint probe_wait; internal uint dead_link; internal uint incr; internal uint current; internal int fastresend; internal int fastlimit; internal bool nocwnd; internal readonly Queue<Segment> snd_queue = new Queue<Segment>(16); internal readonly Queue<Segment> rcv_queue = new Queue<Segment>(16); internal readonly List<Segment> snd_buf = new List<Segment>(16); internal readonly List<Segment> rcv_buf = new List<Segment>(16); internal readonly List<AckItem> acklist = new List<AckItem>(16); internal byte[] buffer; private readonly Action<byte[], int> output; private readonly Pool<Segment> SegmentPool = new Pool<Segment>(() => new Segment(), delegate(Segment segment) { segment.Reset(); }, 32); public int WaitSnd => snd_buf.Count + snd_queue.Count; public Kcp(uint conv, Action<byte[], int> output) { this.conv = conv; this.output = output; snd_wnd = 32u; rcv_wnd = 128u; rmt_wnd = 128u; mtu = 1200u; mss = mtu - 24; rx_rto = 200; rx_minrto = 100; interval = 100u; ts_flush = 100u; ssthresh = 2u; fastlimit = 5; dead_link = 20u; buffer = new byte[(mtu + 24) * 3]; } private Segment SegmentNew() { return SegmentPool.Take(); } private void SegmentDelete(Segment seg) { SegmentPool.Return(seg); } internal uint WndUnused() { if (rcv_queue.Count < rcv_wnd) { return rcv_wnd - (uint)rcv_queue.Count; } return 0u; } public int Receive(byte[] buffer, int len) { if (len < 0) { throw new NotSupportedException("Receive ispeek for negative len is not supported!"); } if (rcv_queue.Count == 0) { return -1; } if (len < 0) { len = -len; } int num = PeekSize(); if (num < 0) { return -2; } if (num > len) { return -3; } bool flag = rcv_queue.Count >= rcv_wnd; int num2 = 0; len = 0; while (rcv_queue.Count > 0) { Segment segment = rcv_queue.Dequeue(); Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, num2, (int)segment.data.Position); num2 += (int)segment.data.Position; len += (int)segment.data.Position; uint frg = segment.frg; SegmentDelete(segment); if (frg == 0) { break; } } int num3 = 0; foreach (Segment item in rcv_buf) { if (item.sn == rcv_nxt && rcv_queue.Count < rcv_wnd) { num3++; rcv_queue.Enqueue(item); rcv_nxt++; continue; } break; } rcv_buf.RemoveRange(0, num3); if (rcv_queue.Count < rcv_wnd && flag) { probe |= 2u; } return len; } public int PeekSize() { int num = 0; if (rcv_queue.Count == 0) { return -1; } Segment segment = rcv_queue.Peek(); if (segment.frg == 0) { return (int)segment.data.Position; } if (rcv_queue.Count < segment.frg + 1) { return -1; } foreach (Segment item in rcv_queue) { num += (int)item.data.Position; if (item.frg == 0) { break; } } return num; } public int Send(byte[] buffer, int offset, int len) { if (len < 0) { return -1; } int num = (int)((len <= mss) ? 1 : ((len + mss - 1) / mss)); if (num > 255) { throw new Exception($"Send len={len} requires {num} fragments, but kcp can only handle up to {255} fragments."); } if (num >= rcv_wnd) { return -2; } if (num == 0) { num = 1; } for (int i = 0; i < num; i++) { int num2 = ((len > (int)mss) ? ((int)mss) : len); Segment segment = SegmentNew(); if (len > 0) { segment.data.Write(buffer, offset, num2); } segment.frg = (uint)(num - i - 1); snd_queue.Enqueue(segment); offset += num2; len -= num2; } return 0; } private void UpdateAck(int rtt) { if (rx_srtt == 0) { rx_srtt = rtt; rx_rttval = rtt / 2; } else { int num = rtt - rx_srtt; if (num < 0) { num = -num; } rx_rttval = (3 * rx_rttval + num) / 4; rx_srtt = (7 * rx_srtt + rtt) / 8; if (rx_srtt < 1) { rx_srtt = 1; } } int value = rx_srtt + Math.Max((int)interval, 4 * rx_rttval); rx_rto = Utils.Clamp(value, rx_minrto, 60000); } internal void ShrinkBuf() { if (snd_buf.Count > 0) { Segment segment = snd_buf[0]; snd_una = segment.sn; } else { snd_una = snd_nxt; } } internal void ParseAck(uint sn) { if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0) { return; } for (int i = 0; i < snd_buf.Count; i++) { Segment segment = snd_buf[i]; if (sn == segment.sn) { snd_buf.RemoveAt(i); SegmentDelete(segment); break; } if (Utils.TimeDiff(sn, segment.sn) < 0) { break; } } } internal void ParseUna(uint una) { int num = 0; foreach (Segment item in snd_buf) { if (Utils.TimeDiff(una, item.sn) > 0) { num++; SegmentDelete(item); continue; } break; } snd_buf.RemoveRange(0, num); } internal void ParseFastack(uint sn, uint ts) { if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0) { return; } foreach (Segment item in snd_buf) { if (Utils.TimeDiff(sn, item.sn) < 0) { break; } if (sn != item.sn) { item.fastack++; } } } private void AckPush(uint sn, uint ts) { acklist.Add(new AckItem { serialNumber = sn, timestamp = ts }); } private void ParseData(Segment newseg) { uint sn = newseg.sn; if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) >= 0 || Utils.TimeDiff(sn, rcv_nxt) < 0) { SegmentDelete(newseg); return; } InsertSegmentInReceiveBuffer(newseg); MoveReceiveBufferReadySegmentsToQueue(); } internal void InsertSegmentInReceiveBuffer(Segment newseg) { bool flag = false; int num; for (num = rcv_buf.Count - 1; num >= 0; num--) { Segment segment = rcv_buf[num]; if (segment.sn == newseg.sn) { flag = true; break; } if (Utils.TimeDiff(newseg.sn, segment.sn) > 0) { break; } } if (!flag) { rcv_buf.Insert(num + 1, newseg); } else { SegmentDelete(newseg); } } private void MoveReceiveBufferReadySegmentsToQueue() { int num = 0; foreach (Segment item in rcv_buf) { if (item.sn == rcv_nxt && rcv_queue.Count < rcv_wnd) { num++; rcv_queue.Enqueue(item); rcv_nxt++; continue; } break; } rcv_buf.RemoveRange(0, num); } public int Input(byte[] data, int offset, int size) { uint earlier = snd_una; uint num = 0u; uint ts = 0u; int num2 = 0; if (data == null || size < 24) { return -1; } while (size >= 24) { offset += Utils.Decode32U(data, offset, out var value); if (value != conv) { return -1; } offset += Utils.Decode8u(data, offset, out var value2); offset += Utils.Decode8u(data, offset, out var value3); offset += Utils.Decode16U(data, offset, out var value4); offset += Utils.Decode32U(data, offset, out var value5); offset += Utils.Decode32U(data, offset, out var value6); offset += Utils.Decode32U(data, offset, out var value7); offset += Utils.Decode32U(data, offset, out var value8); size -= 24; if (size < value8 || (int)value8 < 0) { return -2; } if (value2 != 81 && value2 != 82 && value2 != 83 && value2 != 84) { return -3; } if (value2 == 81 && (value3 >= byte.MaxValue || (uint)(value3 + 1) >= rcv_wnd)) { return -4; } rmt_wnd = value4; ParseUna(value7); ShrinkBuf(); switch (value2) { case 82: if (Utils.TimeDiff(current, value5) >= 0) { UpdateAck(Utils.TimeDiff(current, value5)); } ParseAck(value6); ShrinkBuf(); if (num2 == 0) { num2 = 1; num = value6; ts = value5; } else if (Utils.TimeDiff(value6, num) > 0) { num = value6; ts = value5; } break; case 81: if (Utils.TimeDiff(value6, rcv_nxt + rcv_wnd) >= 0) { break; } AckPush(value6, value5); if (Utils.TimeDiff(value6, rcv_nxt) >= 0) { Segment segment = SegmentNew(); segment.conv = value; segment.cmd = value2; segment.frg = value3; segment.wnd = value4; segment.ts = value5; segment.sn = value6; segment.una = value7; if (value8 != 0) { segment.data.Write(data, offset, (int)value8); } ParseData(segment); } break; case 83: probe |= 2u; break; default: return -3; case 84: break; } offset += (int)value8; size -= (int)value8; } if (num2 != 0) { ParseFastack(num, ts); } if (Utils.TimeDiff(snd_una, earlier) > 0 && cwnd < rmt_wnd) { if (cwnd < ssthresh) { cwnd++; incr += mss; } else { if (incr < mss) { incr = mss; } incr += mss * mss / incr + mss / 16; if ((cwnd + 1) * mss <= incr) { cwnd = (incr + mss - 1) / ((mss == 0) ? 1 : mss); } } if (cwnd > rmt_wnd) { cwnd = rmt_wnd; incr = rmt_wnd * mss; } } return 0; } private void MakeSpace(ref int size, int space) { if (size + space > mtu) { output(buffer, size); size = 0; } } private void FlushBuffer(int size) { if (size > 0) { output(buffer, size); } } public void Flush() { int size = 0; bool flag = false; if (!updated) { return; } Segment segment = SegmentNew(); segment.conv = conv; segment.cmd = 82u; segment.wnd = WndUnused(); segment.una = rcv_nxt; foreach (AckItem item in acklist) { MakeSpace(ref size, 24); segment.sn = item.serialNumber; segment.ts = item.timestamp; size += segment.Encode(buffer, size); } acklist.Clear(); if (rmt_wnd == 0) { if (probe_wait == 0) { probe_wait = 7000u; ts_probe = current + probe_wait; } else if (Utils.TimeDiff(current, ts_probe) >= 0) { if (probe_wait < 7000) { probe_wait = 7000u; } probe_wait += probe_wait / 2; if (probe_wait > 120000) { probe_wait = 120000u; } ts_probe = current + probe_wait; probe |= 1u; } } else { ts_probe = 0u; probe_wait = 0u; } if ((probe & 1) != 0) { segment.cmd = 83u; MakeSpace(ref size, 24); size += segment.Encode(buffer, size); } if ((probe & 2) != 0) { segment.cmd = 84u; MakeSpace(ref size, 24); size += segment.Encode(buffer, size); } probe = 0u; uint num = Math.Min(snd_wnd, rmt_wnd); if (!nocwnd) { num = Math.Min(cwnd, num); } while (Utils.TimeDiff(snd_nxt, snd_una + num) < 0 && snd_queue.Count != 0) { Segment segment2 = snd_queue.Dequeue(); segment2.conv = conv; segment2.cmd = 81u; segment2.wnd = segment.wnd; segment2.ts = current; segment2.sn = snd_nxt; snd_nxt++; segment2.una = rcv_nxt; segment2.resendts = current; segment2.rto = rx_rto; segment2.fastack = 0u; segment2.xmit = 0u; snd_buf.Add(segment2); } uint num2 = ((fastresend > 0) ? ((uint)fastresend) : uint.MaxValue); uint num3 = ((nodelay == 0) ? ((uint)rx_rto >> 3) : 0u); int num4 = 0; foreach (Segment item2 in snd_buf) { bool flag2 = false; if (item2.xmit == 0) { flag2 = true; item2.xmit++; item2.rto = rx_rto; item2.resendts = (uint)((int)current + item2.rto) + num3; } else if (Utils.TimeDiff(current, item2.resendts) >= 0) { flag2 = true; item2.xmit++; xmit++; if (nodelay == 0) { item2.rto += Math.Max(item2.rto, rx_rto); } else { int num5 = ((nodelay < 2) ? item2.rto : rx_rto); item2.rto += num5 / 2; } item2.resendts = current + (uint)item2.rto; flag = true; } else if (item2.fastack >= num2 && (item2.xmit <= fastlimit || fastlimit <= 0)) { flag2 = true; item2.xmit++; item2.fastack = 0u; item2.resendts = current + (uint)item2.rto; num4++; } if (flag2) { item2.ts = current; item2.wnd = segment.wnd; item2.una = rcv_nxt; int space = 24 + (int)item2.data.Position; MakeSpace(ref size, space); size += item2.Encode(buffer, size); if (item2.data.Position > 0) { Buffer.BlockCopy(item2.data.GetBuffer(), 0, buffer, size, (int)item2.data.Position); size += (int)item2.data.Position; } if (item2.xmit >= dead_link) { state = -1; } } } SegmentDelete(segment); FlushBuffer(size); if (num4 > 0) { uint num6 = snd_nxt - snd_una; ssthresh = num6 / 2; if (ssthresh < 2) { ssthresh = 2u; } cwnd = ssthresh + num2; incr = cwnd * mss; } if (flag) { ssthresh = num / 2; if (ssthresh < 2) { ssthresh = 2u; } cwnd = 1u; incr = mss; } if (cwnd < 1) { cwnd = 1u; incr = mss; } } public void Update(uint currentTimeMilliSeconds) { current = currentTimeMilliSeconds; if (!updated) { updated = true; ts_flush = current; } int num = Utils.TimeDiff(current, ts_flush); if (num >= 10000 || num < -10000) { ts_flush = current; num = 0; } if (num >= 0) { ts_flush += interval; if (current >= ts_flush) { ts_flush = current + interval; } Flush(); } } public uint Check(uint current_) { uint num = ts_flush; int num2 = int.MaxValue; if (!updated) { return current_; } if (Utils.TimeDiff(current_, num) >= 10000 || Utils.TimeDiff(current_, num) < -10000) { num = current_; } if (Utils.TimeDiff(current_, num) >= 0) { return current_; } int num3 = Utils.TimeDiff(num, current_); foreach (Segment item in snd_buf) { int num4 = Utils.TimeDiff(item.resendts, current_); if (num4 <= 0) { return current_; } if (num4 < num2) { num2 = num4; } } uint num5 = (uint)((num2 < num3) ? num2 : num3); if (num5 >= interval) { num5 = interval; } return current_ + num5; } public void SetMtu(uint mtu) { if (mtu < 50 || mtu < 24) { throw new ArgumentException("MTU must be higher than 50 and higher than OVERHEAD"); } buffer = new byte[(mtu + 24) * 3]; this.mtu = mtu; mss = mtu - 24; } public void SetInterval(uint interval) { if (interval > 5000) { interval = 5000u; } else if (interval < 10) { interval = 10u; } this.interval = interval; } public void SetNoDelay(uint nodelay, uint interval = 100u, int resend = 0, bool nocwnd = false) { this.nodelay = nodelay; if (nodelay != 0) { rx_minrto = 30; } else { rx_minrto = 100; } if (interval >= 0) { if (interval > 5000) { interval = 5000u; } else if (interval < 10) { interval = 10u; } this.interval = interval; } if (resend >= 0) { fastresend = resend; } this.nocwnd = nocwnd; } public void SetWindowSize(uint sendWindow, uint receiveWindow) { if (sendWindow != 0) { snd_wnd = sendWindow; } if (receiveWindow != 0) { rcv_wnd = Math.Max(receiveWindow, 128u); } } } public class Pool<T> { private readonly Stack<T> objects = new Stack<T>(); private readonly Func<T> objectGenerator; private readonly Action<T> objectResetter; public int Count => objects.Count; public Pool(Func<T> objectGenerator, Action<T> objectResetter, int initialCapacity) { this.objectGenerator = objectGenerator; this.objectResetter = objectResetter; for (int i = 0; i < initialCapacity; i++) { objects.Push(objectGenerator()); } } public T Take() { if (objects.Count <= 0) { return objectGenerator(); } return objects.Pop(); } public void Return(T item) { objectResetter(item); objects.Push(item); } public void Clear() { objects.Clear(); } } internal class Segment { internal uint conv; internal uint cmd; internal uint frg; internal uint wnd; internal uint ts; internal uint sn; internal uint una; internal uint resendts; internal int rto; internal uint fastack; internal uint xmit; internal MemoryStream data = new MemoryStream(1200); internal int Encode(byte[] ptr, int offset) { int num = offset; offset += Utils.Encode32U(ptr, offset, conv); offset += Utils.Encode8u(ptr, offset, (byte)cmd); offset += Utils.Encode8u(ptr, offset, (byte)frg); offset += Utils.Encode16U(ptr, offset, (ushort)wnd); offset += Utils.Encode32U(ptr, offset, ts); offset += Utils.Encode32U(ptr, offset, sn); offset += Utils.Encode32U(ptr, offset, una); offset += Utils.Encode32U(ptr, offset, (uint)data.Position); return offset - num; } internal void Reset() { conv = 0u; cmd = 0u; frg = 0u; wnd = 0u; ts = 0u; sn = 0u; una = 0u; rto = 0; xmit = 0u; resendts = 0u; fastack = 0u; data.SetLength(0L); } } public static class Utils { public static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } public static int Encode8u(byte[] p, int offset, byte value) { p[offset] = value; return 1; } public static int Decode8u(byte[] p, int offset, out byte value) { value = p[offset]; return 1; } public static int Encode16U(byte[] p, int offset, ushort value) { p[offset] = (byte)value; p[1 + offset] = (byte)(value >> 8); return 2; } public static int Decode16U(byte[] p, int offset, out ushort value) { ushort num = 0; num |= p[offset]; num |= (ushort)(p[1 + offset] << 8); value = num; return 2; } public static int Encode32U(byte[] p, int offset, uint value) { p[offset] = (byte)value; p[1 + offset] = (byte)(value >> 8); p[2 + offset] = (byte)(value >> 16); p[3 + offset] = (byte)(value >> 24); return 4; } public static int Decode32U(byte[] p, int offset, out uint value) { uint num = 0u; num |= p[offset]; num |= (uint)(p[1 + offset] << 8); num |= (uint)(p[2 + offset] << 16); num |= (uint)(p[3 + offset] << 24); value = num; return 4; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int TimeDiff(uint later, uint earlier) { return (int)(later - earlier); } } public enum ErrorCode : byte { DnsResolve, Timeout, Congestion, InvalidReceive, InvalidSend, ConnectionClosed, Unexpected } public enum KcpChannel : byte { Reliable = 1, Unreliable } [Serializable] public class KcpConfig { public bool DualMode; public int RecvBufferSize; public int SendBufferSize; public int Mtu; public bool NoDelay; public uint Interval; public int FastResend; public bool CongestionWindow; public uint SendWindowSize; public uint ReceiveWindowSize; public int Timeout; public uint MaxRetransmits; public KcpConfig(bool DualMode = true, int RecvBufferSize = 7340032, int SendBufferSize = 7340032, int Mtu = 1200, bool NoDelay = true, uint Interval = 10u, int FastResend = 0, bool CongestionWindow = false, uint SendWindowSize = 32u, uint ReceiveWindowSize = 128u, int Timeout = 600000, uint MaxRetransmits = 20u) { this.DualMode = DualMode; this.RecvBufferSize = RecvBufferSize; this.SendBufferSize = SendBufferSize; this.Mtu = Mtu; this.NoDelay = NoDelay; this.Interval = Interval; this.FastResend = FastResend; this.CongestionWindow = CongestionWindow; this.SendWindowSize = SendWindowSize; this.ReceiveWindowSize = ReceiveWindowSize; this.Timeout = Timeout; this.MaxRetransmits = MaxRetransmits; } } public enum KcpHeaderReliable : byte { Hello = 1, Ping, Data } public enum KcpHeaderUnreliable : byte { Data = 4, Disconnect } public static class KcpHeader { public static bool ParseReliable(byte value, out KcpHeaderReliable header) { if (Enum.IsDefined(typeof(KcpHeaderReliable), value)) { header = (KcpHeaderReliable)value; return true; } header = KcpHeaderReliable.Ping; return false; } public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header) { if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value)) { header = (KcpHeaderUnreliable)value; return true; } header = KcpHeaderUnreliable.Disconnect; return false; } } public abstract class KcpPeer { internal Kcp kcp; internal uint cookie; protected KcpState state; public const int DEFAULT_TIMEOUT = 600000; public int timeout; private uint lastReceiveTime; private uint currentTime; private readonly byte[] kcpMessageBuffer; private readonly byte[] kcpSendBuffer; private readonly byte[] rawSendBuffer; public const int PING_INTERVAL = 1000; private uint lastPingTime; internal const int QueueDisconnectThreshold = 10000; public const int CHANNEL_HEADER_SIZE = 1; public const int COOKIE_HEADER_SIZE = 4; public const int METADATA_SIZE = 5; public readonly int unreliableMax; public readonly int reliableMax; private bool disconnecting; private bool remoteDisconnectRequested; public int SendQueueCount => kcp.snd_queue.Count; public int ReceiveQueueCount => kcp.rcv_queue.Count; public int SendBufferCount => kcp.snd_buf.Count; public int ReceiveBufferCount => kcp.rcv_buf.Count; public uint MaxSendRate => kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval; public uint MaxReceiveRate => kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval; private static int ReliableMaxMessageSize_Unconstrained(int mtu, uint rcv_wnd) { return (mtu - 24 - 5) * (int)(rcv_wnd - 1) - 1; } public static int ReliableMaxMessageSize(int mtu, uint rcv_wnd) { return ReliableMaxMessageSize_Unconstrained(mtu, Math.Min(rcv_wnd, 255u)); } public static int UnreliableMaxMessageSize(int mtu) { return mtu - 5 - 1; } protected KcpPeer(KcpConfig config, uint cookie) : this(config, cookie, 0u) { } protected KcpPeer(KcpConfig config, uint cookie, uint initialTime) { Reset(config, initialTime); this.cookie = cookie; Log.Info($"[KCP] {GetType()}: created with cookie={cookie}"); rawSendBuffer = new byte[config.Mtu]; unreliableMax = UnreliableMaxMessageSize(config.Mtu); reliableMax = ReliableMaxMessageSize(config.Mtu, config.ReceiveWindowSize); kcpMessageBuffer = new byte[1 + reliableMax]; kcpSendBuffer = new byte[1 + reliableMax]; } protected void Reset(KcpConfig config) { Reset(config, 0u); } protected void Reset(KcpConfig config, uint initialTime) { cookie = 0u; state = KcpState.Connected; lastReceiveTime = initialTime; lastPingTime = initialTime; currentTime = initialTime; kcp = new Kcp(0u, RawSendReliable); kcp.SetNoDelay(config.NoDelay ? 1u : 0u, config.Interval, config.FastResend, !config.CongestionWindow); kcp.SetWindowSize(config.SendWindowSize, config.ReceiveWindowSize); kcp.SetMtu((uint)(config.Mtu - 5)); kcp.dead_link = config.MaxRetransmits; timeout = config.Timeout; } protected void SetInitialTime(uint time) { lastReceiveTime = time; lastPingTime = time; currentTime = time; } protected abstract void OnAuthenticated(); protected abstract void OnData(ArraySegment<byte> message, KcpChannel channel); protected abstract void OnDisconnected(); protected virtual bool OnReliableHelloReceived() { return true; } protected abstract void OnError(ErrorCode error, string message); protected abstract void RawSend(ArraySegment<byte> data); private void HandleTimeout(uint time) { if (Utils.TimeDiff(time, lastReceiveTime) >= timeout) { OnError(ErrorCode.Timeout, $"{GetType()}: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); Disconnect(); } } private void HandleDeadLink() { if (kcp.state == -1) { OnError(ErrorCode.Timeout, $"{GetType()}: dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting."); Disconnect(); } } private void HandlePing(uint time) { if (Utils.TimeDiff(time, lastPingTime) >= 1000) { SendPing(); lastPingTime = time; } } private void HandleChoked() { int num = kcp.rcv_queue.Count + kcp.snd_queue.Count + kcp.rcv_buf.Count + kcp.snd_buf.Count; if (num >= 10000) { OnError(ErrorCode.Congestion, $"{GetType()}: disconnecting connection because it can't process data fast enough.\n" + $"Queue total {num}>{10000}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" + "* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n* Or perhaps the network is simply too slow on our end, or on the other end."); kcp.snd_queue.Clear(); Disconnect(); } } private bool ReceiveNextReliable(uint time, out KcpHeaderReliable header, out ArraySegment<byte> message) { message = default(ArraySegment<byte>); header = KcpHeaderReliable.Ping; int num = kcp.PeekSize(); if (num <= 0) { return false; } if (num > kcpMessageBuffer.Length) { OnError(ErrorCode.InvalidReceive, $"{GetType()}: possible allocation attack for msgSize {num} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); Disconnect(); return false; } int num2 = kcp.Receive(kcpMessageBuffer, num); if (num2 < 0) { OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed with error={num2}. closing connection."); Disconnect(); return false; } byte b = kcpMessageBuffer[0]; if (!KcpHeader.ParseReliable(b, out header)) { OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {b} is not defined in {typeof(KcpHeaderReliable)}."); Disconnect(); return false; } message = new ArraySegment<byte>(kcpMessageBuffer, 1, num - 1); lastReceiveTime = time; return true; } private void TickIncoming_Connected(uint time) { HandleTimeout(time); HandleDeadLink(); HandlePing(time); HandleChoked(); if (ReceiveNextReliable(time, out var header, out var _)) { switch (header) { case KcpHeaderReliable.Hello: Log.Info($"[KCP] {GetType()}: received hello with cookie={cookie}"); if (!OnReliableHelloReceived()) { Disconnect(); break; } state = KcpState.Authenticated; OnAuthenticated(); break; case KcpHeaderReliable.Data: OnError(ErrorCode.InvalidReceive, $"[KCP] {GetType()}: received invalid header {header} while Connected. Disconnecting the connection."); Disconnect(); break; } } if (remoteDisconnectRequested) { remoteDisconnectRequested = false; Disconnect(); } } private void TickIncoming_Authenticated(uint time) { HandleTimeout(time); HandleDeadLink(); HandlePing(time); HandleChoked(); KcpHeaderReliable header; ArraySegment<byte> message; while (ReceiveNextReliable(time, out header, out message)) { switch (header) { case KcpHeaderReliable.Hello: Log.Warning($"{GetType()}: received invalid header {header} while Authenticated. Disconnecting the connection."); Disconnect(); break; case KcpHeaderReliable.Data: if (message.Count > 0) { OnData(message, KcpChannel.Reliable); break; } OnError(ErrorCode.InvalidReceive, $"{GetType()}: received empty Data message while Authenticated. Disconnecting the connection."); Disconnect(); break; } } if (remoteDisconnectRequested) { remoteDisconnectRequested = false; Disconnect(); } } public virtual void TickIncoming(uint time) { currentTime = time; try { switch (state) { case KcpState.Connected: TickIncoming_Connected(time); break; case KcpState.Authenticated: TickIncoming_Authenticated(time); break; case KcpState.Disconnected: break; } } catch (SocketException arg) { OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {arg}. This is fine."); Disconnect(); } catch (ObjectDisposedException arg2) { OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {arg2}. This is fine."); Disconnect(); } catch (Exception arg3) { OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected Exception: {arg3}"); Disconnect(); } } public virtual void TickOutgoing(uint time) { currentTime = time; try { switch (state) { case KcpState.Connected: case KcpState.Authenticated: kcp.Update(time); break; } } catch (SocketException arg) { OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {arg}. This is fine."); Disconnect(); } catch (ObjectDisposedException arg2) { OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {arg2}. This is fine."); Disconnect(); } catch (Exception arg3) { OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected exception: {arg3}"); Disconnect(); } } public virtual void Tick(uint time) { TickIncoming(time); TickOutgoing(time); } protected bool OnRawInputReliable(ArraySegment<byte> message) { int num = kcp.Input(message.Array, message.Offset, message.Count); if (num != 0) { Log.Warning($"[KCP] {GetType()}: Input failed with error={num} for buffer with length={message.Count - 1}"); } return num == 0; } protected void OnRawInputUnreliable(ArraySegment<byte> message) { if (message.Count < 1) { return; } byte b = message.Array[message.Offset]; if (!KcpHeader.ParseUnreliable(b, out var header)) { OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {b} is not defined in {typeof(KcpHeaderUnreliable)}."); Disconnect(); return; } message = new ArraySegment<byte>(message.Array, message.Offset + 1, message.Count - 1); switch (header) { case KcpHeaderUnreliable.Data: if (state == KcpState.Authenticated) { OnData(message, KcpChannel.Unreliable); lastReceiveTime = currentTime; } break; case KcpHeaderUnreliable.Disconnect: Log.Info($"[KCP] {GetType()}: received disconnect message"); remoteDisconnectRequested = true; break; } } private void RawSendReliable(byte[] data, int length) { rawSendBuffer[0] = 1; Utils.Encode32U(rawSendBuffer, 1, cookie); Buffer.BlockCopy(data, 0, rawSendBuffer, 5, length); ArraySegment<byte> data2 = new ArraySegment<byte>(rawSendBuffer, 0, length + 1 + 4); RawSend(data2); } private bool SendReliable(KcpHeaderReliable header, ArraySegment<byte> content) { if (1 + content.Count > kcpSendBuffer.Length) { OnError(ErrorCode.InvalidSend, $"{GetType()}: Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={reliableMax}"); return false; } kcpSendBuffer[0] = (byte)header; if (content.Count > 0) { Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count); } int num = kcp.Send(kcpSendBuffer, 0, 1 + content.Count); if (num < 0) { OnError(ErrorCode.InvalidSend, $"{GetType()}: Send failed with error={num} for content with length={content.Count}"); return false; } return true; } private bool SendUnreliable(KcpHeaderUnreliable header, ArraySegment<byte> content) { if (content.Count > unreliableMax) { Log.Error($"[KCP] {GetType()}: Failed to send unreliable message of size {content.Count} because it's larger than UnreliableMaxMessageSize={unreliableMax}"); return false; } rawSendBuffer[0] = 2; Utils.Encode32U(rawSendBuffer, 1, cookie); rawSendBuffer[5] = (byte)header; if (content.Count > 0) { Buffer.BlockCopy(content.Array, content.Offset, rawSendBuffer, 6, content.Count); } ArraySegment<byte> data = new ArraySegment<byte>(rawSendBuffer, 0, content.Count + 1 + 4 + 1); RawSend(data); return true; } public void SendHello() { Log.Info($"[KCP] {GetType()}: sending handshake to other end with cookie={cookie}"); SendReliable(KcpHeaderReliable.Hello, default(ArraySegment<byte>)); } public bool SendData(ArraySegment<byte> data, KcpChannel channel) { if (data.Count == 0) { OnError(ErrorCode.InvalidSend, $"{GetType()}: tried sending empty message. This should never happen. Disconnecting."); Disconnect(); return false; } switch (channel) { case KcpChannel.Reliable: return SendReliable(KcpHeaderReliable.Data, data); case KcpChannel.Unreliable: return SendUnreliable(KcpHeaderUnreliable.Data, data); default: OnError(ErrorCode.InvalidSend, $"{GetType()}: unknown channel {(byte)channel}."); return false; } } private void SendPing() { SendReliable(KcpHeaderReliable.Ping, default(ArraySegment<byte>)); } private void SendDisconnect() { for (int i = 0; i < 5; i++) { SendUnreliable(KcpHeaderUnreliable.Disconnect, default(ArraySegment<byte>)); } } public virtual void Disconnect() { if (state == KcpState.Disconnected || disconnecting) { return; } disconnecting = true; try { SendDisconnect(); } catch (SocketException) { } catch (ObjectDisposedException) { } finally { Log.Info($"[KCP] {GetType()}: Disconnected."); state = KcpState.Disconnected; disconnecting = false; OnDisconnected(); } } } public enum KcpState { Connected, Authenticated, Disconnected } public static class Log { public static Action<string> Info = Console.WriteLine; public static Action<string> Warning = Console.WriteLine; public static Action<string> Error = Console.Error.WriteLine; } } namespace CardShopCoop { public enum CoopRole { None, Host, Client } public enum InviteState { Off, Resolving, Ready, LanOnly } public class CoopCore : MonoBehaviour { public string StatusLine = "Not connected"; public string ErrorLine = ""; public string HostPassword = ""; public ulong LastFailedLobby; public InviteState InviteStatus; public string InviteCodeText; public string InviteReason; public int PortForwardState; private IConnectionProvider _connectionProvider; private ICoopTransport _providerTransportSource; private CoopRuntimeContext _liveRuntimeContext; private ISteamBridge _steam; private CoopRuntimeBehaviour _runtime; private PersistentRuntimeBehaviour _persistentRuntime; private CoopUI _ui; private readonly MessageRouter _messageRouter = new MessageRouter(); private readonly MainThreadDispatcher _dispatcher = new MainThreadDispatcher(64, 4096, 3); private bool _shutdownCompleted; private bool _externalModsDiscovered; private bool _sessionInGame; private bool _worldReady; private int _localConnectionId = -1; private int _sessionGeneration = 1; private int _inviteGeneration; private ulong _autoJoinSteamLobby; private int _autoHostSlot = -1; private string _autoJoinIp; private int _autoPhase; private float _autoTimer; private string _joinPassword = ""; private static int _unityThreadId; private readonly List<InMsg> _dispatchBuffer = new List<InMsg>(64); private readonly HashSet<int> _dispatchRejectedConnections = new HashSet<int>(); private bool _dispatchBacklogWarned; private const int DispatchBudget = 256; private const int DispatchBacklogCap = 2048; private const int MaxIncomingAdmissionPerFrame = 512; private const int MaxDisconnectResultsPerFrame = 64; public static CoopCore Instance { get; private set; } public static CoopRole Role { get; private set; } public static bool WindowBlocksInput => SessionInputRuntime.WindowBlocksInput; public static bool IsTearingDown { get; private set; } public static bool InSessionWorld { get { if ((Object)(object)Instance != (Object)null) { return Instance._sessionInGame; } return false; } } public string EffectivePlayerName => PresenceApi.LocalPlayerName; public bool UsingSteamPersona => !string.Equals(EffectivePlayerName, CoopPlugin.PlayerName.Value, StringComparison.Ordinal); public ICoopTransport Net { get; private set; } public ISteamBridge Steam => _steam; public bool IsSteamSession { get; private set; } public string LastDisconnectCode { get; private set; } = ""; public string LastDisconnectReason { get; private set; } = ""; public bool LastDisconnectRetryable { get; private set; } internal static int SessionGeneration { get { CoopCore instance = Instance; if (!((Object)(object)instance == (Object)null)) { return Volatile.Read(in instance._sessionGeneration); } return -1; } } internal static int LocalConnectionId { get { if (!((Object)(object)Instance == (Object)null)) { return Instance._localConnectionId; } return -1; } } internal CoopRuntimeContext LiveContext => _liveRuntimeContext; private IConnectionProvider ConnectionProvider { get { return _connectionProvider; } set { if (_connectionProvider == value) { OnProviderTransportChanged(); return; } if (_connectionProvider != null) { _connectionProvider.TransportChanged -= OnProviderTransportChanged; _connectionProvider.PeerConnected -= OnProviderPeerConnected; _connectionProvider.Failed -= OnProviderFailed; } _connectionProvider = value; if (_connectionProvider != null) { _connectionProvider.TransportChanged += OnProviderTransportChanged; _connectionProvider.PeerConnected += OnProviderPeerConnected; _connectionProvider.Connected += OnProviderConnected; _connectionProvider.Failed += OnProviderFailed; } OnProviderTransportChanged(); } } internal static bool IsSessionGeneration(int generation) { CoopCore instance = Instance; if ((Object)(object)instance != (Object)null && generation >= 0) { return Volatile.Read(in instance._sessionGeneration) == generation; } return false; } private void Awake() { Instance = this; _unityThreadId = Thread.CurrentThread.ManagedThreadId; _messageRouter.RegisterCoreAttributedHandlers(this); _dispatcher.Start(); PresenceApi.ConfigureIdentity(delegate { string text = ((_steam == null) ? "" : _steam.LocalPersonaName); return (!string.IsNullOrWhiteSpace(text)) ? text : CoopPlugin.PlayerName.Value; }); SaveTransferApi.ConfigureCore(() => Role, () => SessionGeneration, IsSessionGeneration, (Action action) => _dispatcher.TryEnqueue("save-transfer", action), null, delegate { Send(1, (INetMessage)(object)new FullyJoinedMessage()); }, delegate(string reason) { string text = reason ?? "world transfer failed"; if (!_dispatcher.TryEnqueue("save-transfer-failure", delegate { ErrorLine = text; SetStatus(text); })) { CoopPlugin.Log.LogError((object)("coop: could not publish save-transfer failure on the main thread: " + text)); } }, delegate(PeerConnection connection, string reason) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown Net?.GracefulDisconnect(connection, new DisconnectInfo("world transfer failed: " + reason, false, "transfer_failed", true, (ConnectionState)2)); }); _ui = new CoopUI(); _persistentRuntime = ((Component)this).gameObject.AddComponent<PersistentRuntimeBehaviour>(); _persistentRuntime.InitializePersistent(new CoopRuntimeContext(_messageRouter, InGameLevel, Broadcast, Send, ConnectionIds, PresenceApi.PeerName, delegate(string text, float seconds) { SetStatus(text); }, () => SaveTransferApi.PreloadHold)); SceneManager.sceneLoaded += OnSceneLoaded; ReadCommandLine(); _steam = SteamBridge.TryCreate(); if (_steam == null && PlatformProbe.SteamworksPresent) { CoopPlugin.Log.LogInfo((object)"Steamworks assembly present but not functional; LAN and direct IP only."); } if (_steam != null) { _steam.Init(); _steam.OnError = delegate(string error) { ErrorLine = error ?? "Steam error"; CoopPlugin.Log.LogWarning((object)ErrorLine); }; _steam.OnLobbyLive = delegate(ulong lobby) { SetStatus("Hosting via Steam - click 'Invite friend'"); CoopPlugin.Log.LogInfo((object)("steam: lobby live " + lobby)); }; _steam.OnInviteAccepted = delegate(ulong lobby) { JoinSteamLobby(lobby); }; } RegisterEventListeners(); CatalogApi.EnumLendState(); } private void RegisterEventListeners() { CEventManager.AddListener<CEventPlayer_OnOpenCardPack>((EventDelegate<CEventPlayer_OnOpenCardPack>)OnLocalPackOpened); CEventManager.AddListener<CEventPlayer_GameDataFinishLoaded>((EventDelegate<CEventPlayer_GameDataFinishLoaded>)OnGameDataFinishLoaded); CEventManager instance = CSingleton<CEventManager>.Instance; if ((Object)(object)instance != (Object)null) { CoopPlugin.Log.LogInfo((object)("[events] co-op listeners attached to CEventManager '" + ((Object)((Component)instance).gameObject).name + "'")); } } private void ReadCommandLine() { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i < commandLineArgs.Length; i++) { string text = commandLineArgs[i] ?? ""; ulong result2; if (text.StartsWith("-coopautohost=", StringComparison.Ordinal) && int.TryParse(text.Substring(14), out var result)) { _autoHostSlot = result; } else if (text.StartsWith("-coopautojoin=", StringComparison.Ordinal)) { _autoJoinIp = text.Substring(14); } else if (text == "+connect_lobby" && i + 1 < commandLineArgs.Length && ulong.TryParse(commandLineArgs[i + 1], out result2)) { _autoJoinSteamLobby = result2; } } } public void JoinSteamLobby(ulong lobby, string password = "") { ErrorLine = ""; ClearDisconnectMetadata(); if (Role != CoopRole.None) { ErrorLine = "Already in a session."; return; } if (InGameLevel()) { ErrorLine = "Go to the main menu first, then accept the invite again."; return; } if (_steam == null) { ErrorLine = "This build has no Steam support - use LAN or direct IP."; return; } if (!_steam.SteamAvailable()) { ErrorLine = "Steam isn't running."; return; } BeginSession(CoopRole.Client, steam: true); IsSteamSession = true; _joinPassword = password ?? ""; LastFailedLobby = lobby; try { SetConnectionProvider(new SteamConnectionProvider(_steam)); ConnectionProvider.Join(lobby.ToString(), 0, _joinPassword); SetStatus("Joining Steam lobby..."); } catch (Exception ex) { AbortSessionStart("Could not join: " + ex.Message); } } public void StartHostingSteam(bool isPublic, string lobbyName, string password, int maxPlayers) { ErrorLine = ""; ClearDisconnectMetadata(); if (maxPlayers < 2 || maxPlayers > 250) { ErrorLine = $"Player count must be between {2} and {250}."; return; } if (Role != CoopRole.None) { ErrorLine = "Already in a session."; return; } if (!TryAdmitHostStart(out var error)) { ErrorLine = error; return; } if (_steam == null) { ErrorLine = "This build has no Steam support - use LAN instead."; return; } if (!_steam.SteamAvailable()) { ErrorLine = "Steam isn't running - use LAN instead."; return; } BeginSession(CoopRole.Host, steam: true); HostPassword = password ?? ""; try { SetConnectionProvider(new SteamConnectionProvider(_steam)); ConnectionProvider.Host(0, isPublic, lobbyName, HostPassword, maxPlayers); SetStatus("Creating Steam lobby..."); } catch (Exception ex) { AbortSessionStart("Could not host: " + ex.Message); } } public void OpenSteamInvite() { _steam?.OpenInviteDialog(); } public void StartHosting() { ErrorLine = ""; ClearDisconnectMetadata(); if (Role != CoopRole.None) { ErrorLine = "Already in a session."; return; } if (!TryAdmitHostStart(out var error)) { ErrorLine = error; return; } BeginSession(CoopRole.Host, steam: false); try { SetConnectionProvider(new LanConnectionProvider()); ConnectionProvider.Host(CoopPlugin.Port.Value); SetStatus("Hosting - waiting for a player..."); if (CoopPlugin.AutoLanPassword.Value && string.IsNullOrEmpty(HostPassword)) { HostPassword = GenerateSessionPassword(); } BeginInviteResolve(); } catch (Exception ex) { AbortSessionStart("Could not host: " + ex.Message); } } public void JoinHostedGame(string ip) { JoinHostedGame(ip, CoopPlugin.Port.Value, ""); } public void JoinHostedGame(string ip, int joinPort, string password) { ErrorLine = ""; ClearDisconnectMetadata(); if (Role != CoopRole.None) { ErrorLine = "Already in a session."; return; } if (InGameLevel()) { ErrorLine = "Join from the main menu (Title screen)."; return; } ip = (ip ?? "").Trim(); if (ip.Length == 0) { ErrorLine = "Enter the host's IP address."; return; } CoopPlugin.LastJoinIP.Value = ip; BeginSession(CoopRole.Client, steam: false); _joinPassword = password ?? ""; try { SetConnectionProvider(new LanConnectionProvider()); int port = ((joinPort > 0 && joinPort <= 65535) ? joinPort : CoopPlugin.Port.Value); ConnectionProvider.Join(ip, port); SetStatus("Connecting to " + ip + "..."); } catch (Exception ex) { AbortSessionStart("Could not connect: " + ex.Message); } } public void Disconnect() { ClearDisconnectMetadata(); Shutdown("disconnected"); } public void SendEmote() { PresenceApi.SendEmote(); } private void BeginSession(CoopRole role, bool steam) { _shutdownCompleted = false; _dispatcher.Start(); Interlocked.Increment(ref _sessionGeneration); Role = role; IsSteamSession = steam; _localConnectionId = ((role != CoopRole.Host) ? (-1) : 0); if (role == CoopRole.Client) { SaveTransferApi.SetGuestBorrowedWorld(borrowed: true); } } private void OnProviderConnected() { } private void OnProviderFailed(string reason) { AbortSessionStart(reason ?? "connection failed"); } private void OnProviderTransportChanged() { ICoopTransport coopTransport = _connectionProvider?.Transport; if (coopTransport != _providerTransportSource) { _providerTransportSource = coopTransport; Net = coopTransport; if (Net != null && Role != CoopRole.None && (Object)(object)_runtime == (Object)null) { ActivateLiveModuleHooks(); } } } private void OnProviderPeerConnected(PeerConnection connection) { if (connection != null && Net != null && Role != CoopRole.Host && Role == CoopRole.Client) { if (Thread.CurrentThread.ManagedThreadId == _unityThreadId) { SendHello(); } else if (!TryEnqueueMainThread(SendHello)) { CoopPlugin.Log.LogError((object)"coop: could not queue the application Hello on the Unity thread"); } } } private void SetConnectionProvider(IConnectionProvider provider) { ConnectionProvider = provider ?? throw new ArgumentNullException("provider"); } private void ActivateLiveModuleHooks() { if ((Object)(object)_runtime != (Object)null) { return; } CoopRuntimeContext context = (_liveRuntimeContext = new CoopRuntimeContext(_messageRouter, InGameLevel, Broadcast, Send, ConnectionIds, PresenceApi.PeerName, delegate(string text, float seconds) { SetStatus(text); }, () => SaveTransferApi.PreloadHold, delegate(int connectionId, DisconnectInfo info) { PeerConnection val = ConnectionFor(connectionId); if (val != null) { Net?.GracefulDisconnect(val, info); } }, Relay)); _runtime = ((Role == CoopRole.Host) ? ((CoopRuntimeBehaviour)((Component)this).gameObject.AddComponent<ServerRuntimeBehaviour>()) : ((CoopRuntimeBehaviour)((Component)this).gameObject.AddComponent<ClientRuntimeBehaviour>())); try { _runtime.Initialize(context); } catch { _liveRuntimeContext = null; Object.Destroy((Object)(object)_runtime); _runtime = null; throw; } } private void DeactivateLiveModuleHooks() { if ((Object)(object)_runtime == (Object)null) { return; } CoopRuntimeBehaviour runtime = _runtime; _runtime = null; try { runtime.Shutdown(); } catch (Exception ex) { ManualLogSource log = CoopPlugin.Log; if (log != null) { log.LogError((object)("Co-op runtime shutdown failed: " + ex)); } } finally { Object.Destroy((Object)(object)runtime); } } private void AbortSessionStart(string reason) { ErrorLine = reason ?? "session could not start"; CoopPlugin.Log.LogError((object)("coop: " + ErrorLine)); Shutdown(ErrorLine); } private void SendHello() { CatalogHelloData catalogHelloData = CatalogHandshake.BuildHelloData(); List<string> frozenMessageCatalog = GetFrozenMessageCatalog(); HelloMessage message = new HelloMessage { WireVersion = Msg.WireVersion, Version = "2.0.6", PlayerName = EffectivePlayerName, SteamId = ((_steam == null) ? 0 : _steam.LocalSteamId), Password = _joinPassword, PluginHash = ModParity.PluginHash(), EnumHash = catalogHelloData.EnumHash, CardsHash = catalogHelloData.CardsHash, PluginList = ModParity.PluginList(), CardsList = catalogHelloData.CardsList, EnumIdentity = catalogHelloData.EnumIdentity, GameVersion = (Application.version ?? ""), UnityVersion = (Application.unityVersion ?? ""), MessageCatalog = frozenMessageCatalog, MessageReliability = GetFrozenMessageReliability(frozenMessageCatalog) }; PeerConnection val = ConnectionFor(1); if (val == null || !(Net is ICoopHandshakeTransport coopHandshakeTransport)) { throw new InvalidOperationException("could not send the application Hello: no live handshake transport"); } coopHandshakeTransport.SendHandshake(val, (INetMessage)(object)message); } private static List<string> GetFrozenMessageCatalog() { IReadOnlyList<string> orderedWireNames = MessageRegistry.CurrentSnapshot.OrderedWireNames; List<string> list = new List<string>(orderedWireNames.Count); for (int i = 0; i < orderedWireNames.Count; i++) { string text = orderedWireNames[i]; if (text == null) { throw new InvalidOperationException("the frozen message registry contains a DTO without a Type.FullName"); } list.Add(text); } return list; } private static byte[] GetFrozenMessageReliability(IReadOnlyList<string> messageCatalog) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Invalid comparison between Unknown and I4 if (messageCatalog == null) { throw new ArgumentNullException("messageCatalog"); } byte[] array = new byte[(messageCatalog.Count + 7) / 8]; ProtocolSnapshot currentSnapshot = MessageRegistry.CurrentSnapshot; for (int i = 0; i < messageCatalog.Count; i++) { if (!currentSnapshot.TryGet(messageCatalog[i], out var descriptor)) { throw new InvalidOperationException("the frozen message registry is missing " + messageCatalog[i]); } if ((int)descriptor.Reliability == 1) { array[i / 8] |= (byte)(1 << (i & 7)); } } return array; } private static bool TryValidateMessageReliability(byte[] peerBits, byte[] expectedBits, int messageCount, out string reason) { if (peerBits == null) { reason = "message reliability bitset is missing"; return false; } int num = (messageCount + 7) / 8; if (peerBits.Length != num) { reason = "message reliability bitset length differs (peer " + peerBits.Length + ", local " + num + ")"; return false; } if (expectedBits == null || expectedBits.Length != num) { throw new InvalidOperationException("local message reliability bitset is invalid"); } if (num > 0 && (messageCount & 7) != 0) { byte b = (byte)(255 << (messageCount & 7)); if ((peerBits[num - 1] & b) != 0) { reason = "message reliability bitset has non-zero unused tail bits"; return false; } if ((expectedBits[num - 1] & b) != 0) { throw new InvalidOperationException("local message reliability bitset has non-zero unused tail bits"); } } for (int i = 0; i < num; i++) { if (peerBits[i] != expectedBits[i]) { reason = "message reliability differs at byte " + i; return false; } } reason = null; return true; } private static bool TryValidateMessageCatalog(IList<string> peerCatalog, IReadOnlyList<string> expectedCatalog, out string reason) { if (peerCatalog == null) { reason = "message catalog is missing"; return false; } if (peerCatalog.Count != expectedCatalog.Count) { reason = "message catalog count differs (peer " + peerCatalog.Count + ", local " + expectedCatalog.Count + ")"; return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < peerCatalog.Count; i++) { string text = peerCatalog[i]; if (text == null) { reason = "message catalog contains a null name at index " + i; return false; } if (!hashSet.Add(text)) { reason = "message catalog contains a duplicate name at index " + i; return false; } } for (int j = 0; j < expectedCatalog.Count; j++) { if (!string.Equals(peerCatalog[j], expectedCatalog[j], StringComparison.Ordinal)) { reason = "message catalog differs at index " + j + " (peer '" + BoundedMessageName(peerCatalog[j]) + "', local '" + BoundedMessageName(expectedCatalog[j]) + "')"; return false; } } reason = null; return true; } private static string BoundedMessageName(string name) { if (name == null) { return "<null>"; } if (name.Length > 96) { return name.Substring(0, 96) + "..."; } return name; } private void ActivateMessageIdsBeforeTransfer(ICoopTransport transport, PeerConnection connection, int generation) { if (!IsSessionGeneration(generation)) { throw new InvalidOperationException("session ended before message-id activation"); } transport.ActivateMessageIds(connection); } [MessageHandler(typeof(HelloMessage))] private void HandleHello(MessageContext context, HelloMessage hello) { //IL_0427: Unknown result type (might be due to invalid IL or missing references) //IL_042c: Unknown result type (might be due to invalid IL or missing references) PeerConnection connection = context?.Connection; if (Role != CoopRole.Host || connection == null || hello == null || !ExpectControlState(connection, "Hello", (ConnectionState)1)) { return; } string text = hello.PlayerName ?? ""; if (hello.WireVersion != Msg.WireVersion) { PeerConnection connection2 = connection; int wireVersion = Msg.WireVersion; RejectConn(connection2, "wire protocol mismatch - host uses protocol " + wireVersion); return; } if (!string.Equals(hello.Version, "2.0.6", StringComparison.Ordinal)) { RejectConn(connection, "version mismatch - host runs 2.0.6"); return; } if (!string.Equals(hello.Password ?? "", HostPassword, StringComparison.Ordinal)) { RejectConn(connection, "wrong password", retryable: true); return; } if ((hello.GameVersion ?? "") != (Application.version ?? "") || (hello.UnityVersion ?? "") != (Application.unityVersion ?? "")) { if (!CoopPlugin.AllowCrossBuildJoin.Value) { RejectConn(connection, "your GAME build doesn't match the host's"); return; } CoopPlugin.Log.LogWarning((object)("AllowCrossBuildJoin is ON - accepting " + text + " across game builds; save compatibility is not guaranteed.")); } if (!string.Equals(hello.PluginHash, ModParity.PluginHash(), StringComparison.Ordinal)) { RejectConn(connection, ModParity.DescribeMismatch(hello.PluginList, 1024)); return; } List<string> messageCatalog = GetFrozenMessageCatalog(); if (!TryValidateMessageCatalog(hello.MessageCatalog, messageCatalog, out var reason)) { CoopPlugin.Log.LogWarning((object)("message catalog handshake rejected for connection " + connection.Id + ": " + reason)); RejectConn(connection, "message catalog mismatch: " + reason); return; } byte[] messageReliability = GetFrozenMessageReliability(messageCatalog); if (!TryValidateMessageReliability(hello.MessageReliability, messageReliability, messageCatalog.Count, out var reason2)) { CoopPlugin.Log.LogWarning((object)("message reliability handshake rejected for connection " + connection.Id + ": " + reason2)); RejectConn(connection, "message reliability mismatch: " + reason2); return; } if (!CatalogHandshake.IsIdentityUsable(hello.EnumIdentity)) { CoopPlugin.Log.LogWarning((object)("catalog handshake rejected connection " + connection.Id + ": invalid or empty client enum identity.")); RejectConn(connection, "invalid custom-content catalog from client"); return; } CatalogEnumValidation catalogEnumValidation = CatalogHandshake.ValidateEnums(text, hello.EnumHash, hello.EnumIdentity); if (!catalogEnumValidation.HashesAreConsistent) { string text2 = catalogEnumValidation.HashFailureReason ?? "the catalog hash did not match the decoded catalog"; CoopPlugin.Log.LogWarning((object)("catalog handshake rejected connection " + connection.Id + ": " + text2)); RejectConn(connection, "invalid custom-card catalog: " + text2); return; } if (catalogEnumValidation.Conflicts.Count > 0) { CoopPlugin.Log.LogWarning((object)("catalog handshake rejected connection " + connection.Id + ": custom-content name set differs (" + catalogEnumValidation.Conflicts.Count + " entries).")); RejectConn(connection, CatalogHandshake.DescribeEnumKeysMismatch(catalogEnumValidation.Conflicts)); return; } if (!CatalogHandshake.TryValidateCards(hello.CardsHash, hello.CardsList, out var reason3)) { RejectConn(connection, reason3); return; } string text3 = ResolvePeerName(hello.SteamId, text); PresenceApi.SetPeerName(connection.Id, text3); if (!connection.TryTransition((ConnectionState)2)) { CoopPlugin.Log.LogWarning((object)("Ignoring Hello from connection in invalid phase " + ((object)connection.State/*cast due to .constrained prefix*/).ToString())); return; } try { _runtime?.ClientJoined(connection); } catch (Exception ex) { CoopPlugin.Log.LogError((object)("Feature ClientJoined callbacks failed: " + ex)); } SetStatus("Hosting - " + text3 + " joined!"); CoopPlugin.Log.LogInfo((object)(text3 + " passed admission; starting world transfer")); CatalogWelcomeBlobs blobs = CatalogHandshake.BuildWelcomeBlobs(); ICoopTransport transport = Net; int transferGeneration = SessionGeneration; if (transport == null) { RejectConn(connection, "transport disappeared before world transfer"); } else if (!SaveTransferApi.TryAuthorizeAndSend(connection, delegate(SaveTransferOffer offer) { ((transport as ICoopHandshakeTransport) ?? throw new InvalidOperationException("could not send Welcome: no live handshake transport")).SendHandshake(connection, (INetMessage)(object)new WelcomeMessage { WireVersion = Msg.WireVersion, Version = "2.0.6", HostName = EffectivePlayerName, SteamId = ((_steam == null) ? 0 : _steam.LocalSteamId), SaveLength = offer.SaveLength, BundleLength = offer.BundleLength, SidecarsComplete = offer.SidecarsComplete, SidecarWarning = offer.SidecarWarning, SelfId = connection.Id, HostEnumIdentity = blobs.EnumIdentity, HostCardsBlob = blobs.CardsBlob, MessageCatalog = new List<string>(messageCatalog), MessageReliability = (byte[])messageReliability.Clone() }); return true; }, delegate { ActivateMessageIdsBeforeTransfer(transport, connection, transferGeneration); })) { RejectConn(connection, "could not start the world transfer"); } } [MessageHandler(typeof(WelcomeMessage))] private void HandleWelcome(MessageContext context, WelcomeMessage welcome) { PeerConnection val = context?.Connection; if (Role != CoopRole.Client || val == null || welcome == null || !ExpectControlState(val, "Welcome", (ConnectionState)1)) { return; } if (welcome.WireVersion != Msg.WireVersion || !string.Equals(welcome.Version, "2.0.6", StringComparison.Ordinal)) { Shutdown("plugin or wire version mismatch"); return; } List<string> frozenMessageCatalog = GetFrozenMessageCatalog(); if (!TryValidateMessageCatalog(welcome.MessageCatalog, frozenMessageCatalog, out var reason)) { CoopPlugin.Log.LogWarning((object)("message catalog handshake rejected from host: " + reason)); Shutdown("message catalog mismatch: " + reason); return; } byte[] frozenMessageReliability = GetFrozenMessageReliability(frozenMessageCatalog); if (!TryValidateMessageReliability(welcome.MessageReliability, frozenMessageReliability, frozenMessageCatalog.Count, out var reason2)) { CoopPlugin.Log.LogWarning((object)("message reliability handshake rejected from host: " + reason2)); Shutdown("message reliability mismatch: " + reason2); return; } Net.ActivateMessageIds(val); bool flag = CatalogHandshake.IsIdentityUsable(welcome.HostEnumIdentity); CatalogBlobData catalogBlobData = CatalogHandshake.ReadBlob(welcome.HostCardsBlob); if (!flag || !catalogBlobData.IsValid) { string text = (flag ? ("host CardsBlob: " + catalogBlobData.FailureReason) : "host enum identity is invalid or empty"); CoopPlugin.Log.LogError((object)("catalog handshake received an invalid host blob: " + text)); Shutdown("host sent an invalid custom-card catalog"); return; } CatalogIdMap.Build(welcome.HostEnumIdentity); _localConnectionId = welcome.SelfId; PresenceApi.SetPeerName(1, ResolvePeerName(welcome.SteamId, welcome.HostName)); if (!SaveTransferApi.TryBeginClientTransfer(welcome.SaveLength, welcome.BundleLength, welcome.SidecarsComplete, welcome.SidecarWarning)) { Shutdown("invalid world transfer authorization"); return; } if (!val.TryTransition((ConnectionState)2)) { Shutdown("invalid handshake phase"); return; } try { _runtime?.ClientJoined(val); } catch (Exception ex) { CoopPlugin.Log.LogError((object)("Feature ClientJoined callbacks failed: " + ex)); } SetStatus("Downloading " + (PresenceApi.PeerName(1) ?? "host") + "'s shop..."); } [MessageHandler(typeof(ByeMessage))] private void HandleBye(MessageContext context, ByeMessage message) { PeerConnection connection = context?.Connection; ConnectionState[] array = new ConnectionState[3]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); if (ExpectControlState(connection, "Bye", (ConnectionState[])(object)array)) { string text = (string.IsNullOrEmpty(message?.Reason) ? "the host ended the session" : message.Reason); if (Role == CoopRole.Client) { ErrorLine = text; Shutdown("rejected: " + text); } else { Net?.Kick(connection); } } } private void Send(int connectionId, INetMessage message) { PeerConnection val = ConnectionFor(connectionId); if (val == null || Net == null) { CoopPlugin.Log.LogError((object)("application send failed: no live connection " + connectionId + " for message " + ((object)message)?.GetType().FullName)); throw new InvalidOperationException("application send has no live connection " + connectionId + " for message " + ((object)message)?.GetType().FullName); } Net.Send(val, message); } private void Broadcast(INetMessage message) { if (Net == null) { CoopPlugin.Log.LogError((object)("application broadcast failed: no live transport for message " + ((object)message)?.GetType().FullName)); throw new InvalidOperationException("application broadcast has no live transport for message " + ((object)message)?.GetType().FullName); } Net.Broadcast(message); } private void Relay(int exceptConnectionId, INetMessage message) { if (Net == null) { CoopPlugin.Log.LogError((object)("application relay failed: no live transport for message " + ((object)message)?.GetType().FullName)); throw new InvalidOperationException("application relay has no live transport for message " + ((object)message)?.GetType().FullName); } ((Net as ICoopRelayTransport) ?? throw new InvalidOperationException("the live transport has no relay seam for message " + ((object)message)?.GetType().FullName)).Relay(message, exceptConnectionId); } private PeerConnection ConnectionFor(int id) { if (Net == null) { return null; } foreach (PeerConnection connection in Net.Connections) { if (connection.Id == id) { return connection; } } return null; } private IReadOnlyList<int> ConnectionIds() { if (Net != null) { return Net.Connections.Select((PeerConnection c) => c.Id).ToArray(); } return Array.Empty<int>(); } private void RejectConn(PeerConnection connection, string reason, bool retryable = false) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown if (connection != null) { CoopPlugin.Log.LogWarning((object)("rejected connection " + connection.Id + ": " + reason)); Net?.GracefulDisconnect(connection, new DisconnectInfo(reason, false, "rejected", retryable, (ConnectionState)1)); } } private void Shutdown(string reason, DisconnectInfo detail = null) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) if (_shutdownCompleted) { return; } _shutdownCompleted = true; IsTearingDown = true; Interlocked.Increment(ref _sessionGeneration); _dispatcher.InvalidateEpoch(); if (Net != null) { foreach (PeerConnection connection in Net.Connections) { Net.GracefulDisconnect(connection, (DisconnectInfo)(((object)detail) ?? ((object)new DisconnectInfo(reason ?? "session ended", false, "shutdown", true, (ConnectionState)4)))); } Net.Stop(); ConnectionEvent result; while (Net.Disconnects.TryDequeue(out result)) { _runtime?.ClientDisconnected(result.Connection, result.Disconnect); } } try { DeactivateLiveModuleHooks(); } finally { try { _persistentRuntime?.SessionStopped(); } finally { ConnectionProvider?.Dispose(); ConnectionProvider = null; Net = null; _liveRuntimeContext = null; } } SaveTransferApi.ClearBorrowedWorldIfSafe(InGameLevel()); SaveTransferRuntimeReset(); PresenceApi.ClearPeerNames(); HudApi.Clear(); CatalogIdMap.Clear(); WorldHostBehaviour.ResetCardState(); WorldClientBehaviour.ResetCardState(); SceneRef.ClearAll(); _dispatchBuffer.Clear(); _dispatchRejectedConnections.Clear(); _localConnectionId = -1; _sessionInGame = false; IsSteamSession = false; HostPassword = ""; _joinPassword = ""; _steam?.Leave(); Application.runInBackground = false; Role = CoopRole.None; IsTearingDown = false; SetStatus((reason == null) ? "Not connected" : ("Not connected (" + reason + ")")); } private static void SaveTransferRuntimeReset() { SaveTransferRuntime.ResetForShutdown(); } private void Update() { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) FrameCounter.Tick(); if (!_externalModsDiscovered) { _externalModsDiscovered = true; ExternalCoopMods.Instance.EnsureDiscovered(); _persistentRuntime?.IncludeExternalPersistentBehaviours(); ExternalCoopMods.Instance.NotifyPersistentScanComplete(); } PerfProbe.BeginFrame(); PerfProbe.FlushThreadMetrics(); using (PerfProbe.Sample("core.dispatcher")) { _dispatcher.Drain(); } using (PerfProbe.Sample("core.auto-tick")) { AutoTick(Time.deltaTime); } if (Input.GetKeyDown(CoopPlugin.UiToggleKey.Value)) { _ui.Visible = !_ui.Visible; } SessionInputRuntime.SetWindowModal(_ui.Visible && InGameplayScene()); if (Role != CoopRole.None && Input.GetKeyDown(CoopPlugin.EmoteKey.Value) && !CoopUI.TextFieldFocused && !NativeTextInputFocused()) { SendEmote(); } if (SaveTransferApi.GuestBorrowedWorld && Role == CoopRole.None && !InGameLevel()) { SaveTransferApi.ClearBorrowedWorldIfSafe(inGame: false); } if (Net == null) { return; } if (!_sessionInGame && InGameLevel()) { _sessionInGame = true; CoopPlugin.Log.LogInfo((object)"co-op session world gate opened"); } Application.runInBackground = true; using (PerfProbe.Sample("core.disconnect-drain")) { DrainDisconnects(); } if (!SaveTransferApi.PreloadHold) { using (PerfProbe.Sample("core.admit-incoming")) { AdmitIncoming(); } using (PerfProbe.Sample("core.dispatch-incoming")) { DispatchIncoming(); } _ = Net; } } private void DrainDisconnects() { int num = 0; ConnectionEvent result; while (num++ < 64 && Net != null && Net.Disconnects.TryDequeue(out result)) { int id = result.Connection.Id; _runtime?.ClientDisconnected(result.Connection, result.Disconnect); PresenceApi.RemovePeer(id); if (Role == CoopRole.Client) { DisconnectInfo disconnect = result.Disconnect; RememberDisconnect(disconnect); ErrorLine = ((disconnect == null) ? "Lost connection to the host." : ("Host disconnected (" + disconnect.Code + "): " + disconnect.Reason)); CoopPlugin.Log.LogInfo((object)("host disconnected: " + (((disconnect != null) ? disconnect.Code : null) ?? "?") + " / " + (((disconnect != null) ? disconnect.Reason : null) ?? "no detail"))); Shutdown(((disconnect != null) ? disconnect.Reason : null) ?? "host connection lost", disconnect); break; } ManualLogSource log = CoopPlugin.Log; string[] obj = new string[6] { "peer ", id.ToString(), " disconnected: ", null, null, null }; DisconnectInfo disconnect2 = result.Disconnect; obj[3] = ((disconnect2 != null) ? disconnect2.Code : null) ?? "?"; obj[4] = " / "; DisconnectInfo disconnect3 = result.Disconnect; obj[5] = ((disconnect3 != null) ? disconnect3.Reason : null) ?? "no detail"; log.LogInfo((object)string.Concat(obj)); SetStatus((Net.ConnectionCount == 0) ? "Hosting - waiting for a player..." : ("Hosting - " + Net.ConnectionCount + " player(s)")); } } private void AdmitIncoming() { int num = 0; InMsg message; while (num++ < 512 && Net != null && Net.TryDequeueIncoming(out message)) { if (message.Connection != null && _dispatchRejectedConnections.Contains(message.Connection.Id)) { continue; } if (_dispatchBuffer.Count < 2048) { _dispatchBuffer.Add(message); continue; } int num2 = FindBufferedTransient(); if (num2 >= 0) { InMsg message2 = _dispatchBuffer[num2]; _dispatchBuffer.RemoveAt(num2); LogDroppedTransient(message2); _dispatchBuffer.Add(message); } else if (IsReliable(message)) { RejectDispatchOverload(message); } else { LogDroppedTransient(message); } } if (_dispatchBuffer.Count < 1024) { _dispatchBacklogWarned = false; } PerfProbe.RecordQueueDepth("core.dispatch", _dispatchBuffer.Count, 2048); } private void DispatchIncoming() { int num = 0; int num2 = 0; int num3 = 0; for (int i = 0; i < _dispatchBuffer.Count; i++) { InMsg message = _dispatchBuffer[i]; if (message.Message == null) { num = i + 1; continue; } int num4 = DispatchCost(message); if (num3 > 0 && num2 + num4 > 256) { break; } num3++; num2 += num4; try { Dispatch(message); } catch (ReliableMessageHandlerException ex) { MarkDispatchConnectionRejected(message.Connection); ManualLogSource log = CoopPlugin.Log; string[] obj = new string[6] { "reliable dispatch failed for connection ", null, null, null, null, null }; PeerConnection connection = message.Connection; obj[1] = ((connection != null) ? new int?(connection.Id) : ((int?)null)).ToString(); obj[2] = " type="; obj[3] = message.MessageType?.FullName; obj[4] = "; the peer was disconnected and the remaining messages were held: "; obj[5] = ex.InnerException?.ToString(); log.LogError((object)string.Concat(obj)); num = i + 1; break; } catch (Exception ex2) { if (IsReliable(message)) { RequestReliableDispatchRecovery(message, ex2); num = i + 1; break; } ManualLogSource log2 = CoopPlugin.Log; string[] obj2 = new string[6] { "transient dispatch conn=", null, null, null, null, null }; PeerConnection connection2 = message.Connection; obj2[1] = ((connection2 != null) ? new int?(connection2.Id) : ((int?)null)).ToString(); obj2[2] = " type="; obj2[3] = message.MessageType?.FullName; obj2[4] = " failed; dropping message: "; obj2[5] = ex2?.ToString(); log2.LogWarning((object)string.Concat(obj2)); } num = i + 1; if (Net == null) { break; } } if (num >= _dispatchBuffer.Count) { _dispatchBuffer.Clear(); } else if (num > 0) { _dispatchBuffer.RemoveRange(0, num); } PerfProbe.RecordQueueDepth("core.dispatch", _dispatchBuffer.Count, 2048); } private bool Dispatch(InMsg message) { //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Expected O, but got Unknown if (message.Connection == null || Net == null || _dispatchRejectedConnections.Contains(message.Connection.Id) || !IsActiveConnection(message.Connection)) { return false; } MessageContext context = new MessageContext { Connection = message.Connection, InGame = InGameLevel(), WorkCost = DispatchCost(message), Transport = Net }; if (_messageRouter.DispatchCore(context, message.Message)) { return true; } if (message.ReceivedBeforeMessageIdActivation) { Net.GracefulDisconnect(message.Connection, new DisconnectInfo("non-handshake message received before catalog activation", false, "protocol_violation", false, message.Connection.State)); } return false; } private static int DispatchCost(InMsg message) { if (message.Message is CardDeltaBatchMessage cardDeltaBatchMessage) { int val = cardDeltaBatchMessage.Deltas?.Count ?? 0; return Math.Max(1, Math.Min(val, 200)); } if (message.Message is PricingDeltaBatchMessage pricingDeltaBatchMessage) { int val2 = (pricingDeltaBatchMessage.Items?.Count ?? 0) + (pricingDeltaBatchMessage.Cards?.Count ?? 0); return Math.Max(1, Math.Min(val2, 200)); } return 1; } private int FindBufferedTransient() { for (int i = 0; i < _dispatchBuffer.Count; i++) { if (IsTransient(_dispatchBuffer[i])) { return i; } } return -1; } private static bool IsReliable(InMsg message) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 if (message.Message == null) { return true; } if (!MessageRegistry.CurrentSnapshot.TryGet(message.MessageType, out var descriptor)) { return true; } return (int)descriptor.Reliability == 0; } private static bool IsTransient(InMsg message) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 if (message.Message != null && MessageRegistry.CurrentSnapshot.TryGet(message.MessageType, out var descriptor)) { return (int)descriptor.Reliability == 1; } return false; } private void RejectDispatchOverload(InMsg message) { //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Expected O, but got Unknown PeerConnection connection = message.Connection; string text = message.MessageType?.FullName ?? "<unknown>"; if (connection == null) { CoopPlugin.Log.LogError((object)("dispatch overload rejected reliable " + text + " without a peer connection")); } else if (_dispatchRejectedConnections.Add(connection.Id)) { string text2 = "dispatch overload: reliable backlog exhausted; reconnect to recover"; CoopPlugin.Log.LogError((object)("dispatch overload on connection " + connection.Id + ": rejecting reliable " + text + " (capacity " + 2048 + ")")); Net?.GracefulDisconnect(connection, new DisconnectInfo(text2, false, "dispatch_overload", true, connection.State)); } } private void LogDroppedTransient(InMsg message) { if (!_dispatchBacklogWarned) { _dispatchBacklogWarned = true; ManualLogSource log = CoopPlugin.Log; string? obj = message.MessageType?.FullName ?? "<unknown>"; PeerConnection connection = message.Connection; log.LogWarning((object)("dispatch backlog cap reached; dropping transient " + obj + " from connection " + ((connection != null) ? new int?(connection.Id) : ((int?)null)))); } } private void RequestReliableDispatchRecovery(InMsg message, Exception error) { //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown MarkDispatchConnectionRejected(message.Connection); ManualLogSource log = CoopPlugin.Log; string[] obj = new string[6] { "reliable dispatch conn=", null, null, null, null, null }; PeerConnection connection = message.Connection; obj[1] = ((connection != null) ? new int?(connection.Id) : ((int?)null)).ToString(); obj[2] = " type="; obj[3] = message.MessageType?.FullName; obj[4] = " failed; requesting session recovery: "; obj[5] = error?.ToString(); log.LogError((object)string.Concat(obj)); if (message.Connection != null) { Net?.GracefulDisconnect(message.Connection, new DisconnectInfo("reliable dispatch failed; session recovery required", false, "dispatch_failed", true, message.Connection.State)); } } private void MarkDispatchConnectionRejected(PeerConnection connection) { if (connection != null) { _dispatchRejectedConnections.Add(connection.Id); } } private bool IsActiveConnection(PeerConnection connection) { foreach (PeerConnection item in Net?.Connections ?? Array.Empty<PeerConnection>()) { if (item == connection) { return true; } } return false; } private static bool ExpectControlState(PeerConnection connection, string control, params ConnectionState[] expected) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) if (connection == null || expected.All((ConnectionState state) => connection.State != state)) { ManualLogSource log = CoopPlugin.Log; string[] obj = new string[6] { "Ignoring late/duplicate ", control, " from connection ", null, null, null }; PeerConnection obj2 = connection; obj[3] = ((obj2 != null) ? new int?(obj2.Id) : ((int?)null)).ToString(); obj[4] = " in phase "; PeerConnection obj3 = connection; obj[5] = ((obj3 != null) ? new ConnectionState?(obj3.State) : ((ConnectionState?)null)).ToString(); log.LogWarning((object)string.Concat(obj)); return false; } return true; } private string ResolvePeerName(ulong steamId, string wireName) { string result = (string.IsNullOrWhiteSpace(wireName) ? "Player" : wireName); if (steamId == 0L || _steam == null) { return result; } string text = _steam.FriendNickname(steamId); if (!string.IsNullOrWhiteSpace(text)) { return text; } return result; } private void SetStatus(string status) { StatusLine = status ?? ""; HudApi.SetStatusLine(StatusLine, 8f); } private void Guarded(string stage, Action action) { try { PerfProbe.Measure(stage, action); } catch (Exception e) { ModuleGuard.Log(stage, e); } } private void RememberDisconnect(DisconnectInfo detail) { if (detail != null) { LastDisconnectCode = detail.Code; LastDisconnectReason = detail.Reason; LastDisconnectRetryable = detail.Retryable; } } private void ClearDisconnectMetadata() { LastDisconnectCode = ""; LastDisconnectReason = ""; LastDisconnectRetryable = false; LastFailedLobby = 0uL; } private void OnLocalPackOpened(CEventPlayer_OnOpenCardPack eventData) { if (Role != CoopRole.None) { ICoopTransport net = Net; if (net != null && net.ConnectionCount > 0) { PresenceApi.SendActivity((EItemType)eventData.m_PackIndex); } } } private void OnGameDataFinishLoaded(CEventPlayer_GameDataFinishLoaded _) { _worldReady = true; CoopPlugin.Log.LogInfo((object)"co-op: shop data finished loading; hosting is now admitted"); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) _worldReady = false; RegisterEventListeners(); SceneRef.NotifySceneLoaded(scene); if (((Scene)(ref scene)).name == "Title" && Role == CoopRole.Client && Net != null) { Shutdown("left the session"); } else if (((Scene)(ref scene)).name != "Title" && Role != CoopRole.None && Net != null && !SaveTransferApi.ClientReloading) { Shutdown("left the session (world reloaded)"); } } private bool TryAdmitHostStart(out string error) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); CGameManager val = GameManager(); bool flag = (Object)(object)val != (Object)null && val.m_IsGameLevel; bool worldReady = _worldReady; bool guestBorrowedWorld = SaveTransferApi.GuestBorrowedWorld; if (flag && worldReady && !guestBorrowedWorld) { error = null; return true; } ShelfManager val2 = SceneRef<ShelfManager>.Get(); error = (guestBorrowedWorld ? "Leave the joined shop and load your own shop before hosting." : ((!flag) ? "Load your shop first, then host." : "Wait for your shop to finish loading, then host.")); CoopPlugin.Log.LogWarning((object)("host start rejected: scene=" + ((Scene)(ref activeScene)).name + " manager=" + (((Object)(object)val == (Object)null) ? "<null>" : ((object)val).GetType().FullName) + " game-level=" + flag + " ready=" + worldReady + " borrowed=" + guestBorrowedWorld + " shelf-data-loaded=" + ((Object)(object)val2 != (Object)null && val2.m_FinishLoadingObjectData) + " reason=" + error)); return false; } private static CGameManager GameManager() { CGameManager instance = CGameManager.m_Instance; if (!((Object)(object)instance != (Object)null)) { return SceneRef<CGameManager>.Get(); } return instance; } private bool InGameLevel() { CGameManager val = GameManager(); if ((Object)(object)val != (Object)null) { return val.m_IsGameLevel; } return false; } private static bool InGameplayScene() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); if (((Scene)(ref activeScene)).isLoaded) { return ((Scene)(ref activeScene)).name != "Title"; } return false; } private void AutoTick(float deltaTime) { if (_autoPhase >= 99 || (_autoHostSlot < 0 && _autoJoinIp == null && _autoJoinSteamLobby == 0L)) { return; } _autoTimer += deltaTime; if (_autoHostSlot >= 0) { if (_autoPhase == 0 && _autoTimer > 6f && !InGameLevel()) { SaveTransferApi.ForceLoadSlot(_autoHostSlot); _autoPhase = 1; _autoTimer = 0f; } else if (_autoPhase == 1 && InGameLevel() && _worldReady) { _autoPhase = 2; _autoTimer = 0f; } else if (_autoPhase == 2 && _autoTimer > 3f) { StartHosting(); _autoPhase = 99; } } else if (_autoJoinIp != null && _autoPhase == 0 && _autoTimer > 10f && !InGameLevel()) { JoinHostedGame(_autoJoinIp); _autoPhase = 99; } else if (_autoJoinSteamLobby != 0L && _steam != null && _autoPhase == 0 && _autoTimer > 10f && !InGameLevel()) { JoinSteamLobby(_autoJoinSteamLobby); _autoPhase = 99; } } private void BeginInviteResolve() { InviteStatus = InviteState.Resolving; InviteCodeText = null; InviteReason = null; PortForwardState = 0; int port = CoopPlugin.Port.Value; string password = HostPassword ?? ""; int generation = Interlocked.Increment(ref _inviteGeneration); Thread thread = new Thread((ThreadStart)delegate { string publicIp = null; string reason = null; try { NetHelpers.BeginDiscovery(); if (CoopPlugin.AutoPortForward.Value) { string reason2; long epoch; bool mapped = NetHelpers.TryMapPort(port, out reason2, out epoch); Publish(generation, delegate { PortForwardState = (mapped ? 1 : 2); }); if (mapped) { publicIp = NetHelpers.UpnpExternalIp(); if (publicIp != null && !NetHelpers.IsPublicIPv4(publicIp)) { reason = "your router's internet address is private (carrier-grade NAT)"; publicIp = null; } } else if (reason2 != null) { CoopPlugin.Log.LogInfo((object)("automatic port forwarding did not happen: " + reason2)); } } if (publicIp == null) { publicIp = NetHelpers.StunPublicIp(); } if (publicIp != null && !NetHelpers.IsPublicIPv4(publicIp)) { publicIp = null; } } catch (Exception ex) { reason = ex.Message; CoopPlugin.Log.LogWarning((object)("invite code resolve failed: " + ex)); } string text = publicIp ?? NetHelpers.LocalIPv4ForGateway() ?? NetHelpers.LocalIPv4(); string code = ((text == null) ? null : InviteCode.Encode(text, port, password)); Publish(generation, delegate { InviteCodeText = code; InviteReason = reason; InviteStatus = ((publicIp == null) ? InviteState.LanOnly : InviteState.Ready); }); }); thread.IsBackground = true; thread.Name = "CoopInvite"; thread.Start(); } private void Publish(int generation, Action action) { _dispatcher.TryEnqueue("invite-publish", delegate { if (Volatile.Read(in _inviteGeneration) == generation) { action(); } }); } private static string GenerateSessionPassword() { byte[] array = new byte[8]; using (RNGCryptoServiceProvider rNGCryptoServiceProvider = new RNGCryptoServiceProvider()) { rNGCryptoServiceProvider.GetBytes(array); } char[] array2 = new char[array.Length]; for (int i = 0; i < array2.Length; i++) { array2[i] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"[array[i] & 0x1F]; } return new string(array2); } private void OnGUI() { using (PerfProbe.Sample("core.ongui")) { _ui.Draw(this, Net); } } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; CEventManager.RemoveListener<CEventPlayer_OnOpenCardPack>((EventDelegate<CEventPlayer_OnOpenCardPack>)OnLocalPackOpened); CEventManager.RemoveListener<CEventPlayer_GameDataFinishLoaded>((EventDelegate<CEventPlayer_GameDataFinishLoaded>)OnGameDataFinishLoaded); Shutdown("plugin unloaded"); try { _persistentRuntime?.Shutdown(); } catch (Exception ex) { ManualLogSource log = CoopPlugin.Log; if (log != null) { log.LogError((object)("Co-op persistent runtime shutdown failed: " + ex)); } } if ((Object)(object)_persistentRuntime != (Object)null) { Object.Destroy((Object)(object)_persistentRuntime); _persistentRuntime = null; } PerfProbe.Reset(); _dispatcher.Stop(); if (Instance == this) { Instance = null; } } private void OnApplicationQuit() { Shutdown("game closed"); } [MessageHandler(typeof(FullyJoinedMessage))] private void HandleFullyJoinedMessage(MessageContext context, FullyJoinedMessage message) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) if (Role != CoopRole.Host || context.Connection == null) { return; } if ((int)context.Connection.State != 2) { CoopPlugin.Log.LogWarning((object)("Ignoring late/duplicate FullyJoined from connection " + context.Connection.Id + " in phase " + ((object)context.Connection.State/*cast due to .constrained prefix*/).ToString())); return; } if (!SaveTransferApi.TryConsumeHostTransferAcknowledgement(context.Connection)) { CoopPlugin.Log.LogWarning((object)("Ignoring premature FullyJoined from connection " + context.Connection.Id + " before its world transfer was fully queued")); return; } try { _runtime?.FullyJoined(context.Connection); } catch (Exception ex) { CoopPlugin.Log.LogError((object)("Feature FullyJoined callbacks failed: " + ex)); } Send(context.ConnectionId, (INetMessage)(object)new FullyJoinedAckMessage()); if (!context.Connection.TryTransition((ConnectionState)3)) { CoopPlugin.Log.LogWarning((object)("Could not complete FullyJoined transition for connection " + context.Connection.Id + " in phase " + ((object)context.Connection.State/*cast due to .constrained prefix*/).ToString())); } } [MessageHandler(typeof(FullyJoinedAckMessage))] private void HandleFullyJoinedAckMessage(MessageContext context, FullyJoinedAckMessage message) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) if (Role != CoopRole.Client || context.Connection == null) { return; } if ((int)context.Connection.State != 2) { CoopPlugin.Log.LogWarning((object)("Ignoring late/duplicate FullyJoinedAck from connection " + context.Connection.Id + " in phase " + ((object)context.Connection.State/*cast due to .constrained prefix*/).ToString())); } else if (!SaveTransferApi.TryConsumeClientTransferAcknowledgement(context.Connection)) { CoopPlugin.Log.LogWarning((object)"Ignoring premature FullyJoinedAck before the client finished loading the transferred world"); } else if (context.Connection.TryTransition((ConnectionState)3)) { try { _runtime?.FullyJoined(context.Connection); } catch (Exception ex) { CoopPlugin.Log.LogError((object)("Feature FullyJoined callbacks failed: " + ex)); } } } [MessageHandler(typeof(DisconnectMessage))] private void HandleDisconnectMessage(MessageContext context, DisconnectMessage message) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown if (context.Connection == null) { CoopPlugin.Log.LogWarning((object)"Ignoring disconnect control without a connection"); return; } ConnectionState val = (ConnectionState)(Enum.IsDefined(typeof(ConnectionState), message.Phase) ? message.Phase : 4); DisconnectInfo val2 = new DisconnectInfo(message.Reason, true, message.Code, message.Retryable, val); if (Role == CoopRole.Client) { RememberDisconnect(val2); Shutdown("remote: " + val2.Reason, val2); } } internal static void EnqueueMainThread(Action action) { if (action == null || TryEnqueueMainThread(action)) { return; } throw new InvalidOperationException("CoopCore is not running"); } internal static bool TryEnqueueMainThread(Action action) { if (action == null) { return false; } CoopCore instance = Instance; if ((Object)(object)instance == (Object)null) { return false; } return instance._dispatcher.TryEnqueue("external-main-thread", action); } public static bool TryGetLocalPlayerPosition(out Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) position = default(Vector3); return PresenceApi.TryGetLocalPlayerPosition(out position); } internal static bool NativeTextInputFocused() { EventSystem current = EventSystem.current; GameObject val = ((current != null) ? current.currentSelectedGameObject : null); if ((Object)(object)val == (Object)null) { return false; } TMP_InputField component = val.GetComponent<TMP_InputField>(); if ((Object)(object)component != (Object)null) { return component.isFocused; } return false; } } public enum GradedAlertMode { Always, OncePerSession, Never } [BepInPlugin("dev.meepen.cardshopcoop", "CardShopCoopCommunity", "2.0.6")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class CoopPlugin : BaseUnityPlugin { public const string Guid = "dev.meepen.cardshopcoop"; public const string Name = "CardShopCoopCommunity"; public static ManualLogSource Log; public static ConfigEntry<int> Port; public static ConfigEntry<string> LastJoinIP; public static ConfigEntry<string> PlayerName; public static ConfigEntry<float> SendRateHz; public static ConfigEntry<bool> AvatarsEnabled; public static ConfigEntry<bool> AllowNsfw; public static ConfigEntry<KeyCode> UiToggleKey; public static ConfigEntry<KeyCode> EmoteKey; public static ConfigEntry<int> ClientWorldSlot; public static ConfigEntry<float> ServeReach; public static ConfigEntry<bool> AllowCrossBuildJoin; public static ConfigEntry<bool> AutoPortForward; public static ConfigEntry<bool> AutoLanPassword; public static ConfigEntry<GradedAlertMode> GradedDriftAlert; public static ConfigEntry<bool> PerfDebug; public static ConfigEntry<int> ArtificialLagMs; public static ConfigEntry<int> ArtificialJitterMs; public static ConfigEntry<bool> EnableGameCheatMenu; public static ConfigEntry<bool> ShowHiddenCategory; public const string Version = "2.0.6"; private void Awake() { //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Invalid comparison between Unknown and I4 //IL_0443: Unknown result type (might be due to invalid IL or missing references) //IL_03bc: Unknown result type (might be due to invalid IL or missing references) //IL_03c2: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; CoopApi.Binding = (ICoopBinding)(object)ExternalCoopMods.Instance; CoopApi.Log = ((BaseUnityPlugin)this).Logger; SceneRef.Install(); ScreenSingletonProbe.Install(); try { Assembly.Load("Newtonsoft.Json"); ((BaseUnityPlugin)this).Logger.LogInfo((object)"network payload serializer: Newtonsoft.Json"); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("network payload serializer unavailable: Newtonsoft.Json.dll is required (" + ex.Message + ")")); } FileLog.Init(Paths.GameRootPath); ((BaseUnityPlugin)this).Logger.LogEvent += delegate(object _, LogEventArgs e2) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) FileLog.Write($"{e2.Level,-7} {e2.Data}"); }; Port = ((BaseUnityPlugin)this).Config.Bind<int>("Network", "Po
BepInEx/plugins/CardShopCoop/CardShopCoop.Api.dll
Decompiled 13 hours agousing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx.Logging; using CardShopCoop.Net; using CardShopCoop.Net.Connection; using CardShopCoop.Net.Protocol; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("CardShopCoop")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Devin 'Meepen' Korb")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Public integration contract for CardShopCoopCommunity. Optional-dependency safe.")] [assembly: AssemblyFileVersion("2.0.6.0")] [assembly: AssemblyInformationalVersion("2.0.6+7377084dcc6c7363bdd27c3bdca151eaad5dfb45")] [assembly: AssemblyProduct("CardShopCoopCommunity.Api")] [assembly: AssemblyTitle("CardShopCoop.Api")] [assembly: AssemblyVersion("2.0.6.0")] namespace CardShopCoop.Net { public interface INetMessage { } [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class NetworkMessageAttribute : Attribute { public Reliability Reliability { get; set; } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public sealed class MessageHandlerAttribute : Attribute { public Type MessageType { get; } public MessageHandlerAttribute(Type messageType) { MessageType = messageType ?? throw new ArgumentNullException("messageType"); } } } namespace CardShopCoop.Net.Protocol { public enum Reliability { Reliable, Transient } } namespace CardShopCoop.Net.Connection { public enum ConnectionState { Connected, Handshaking, Transferring, FullyJoined, Disconnecting, Disconnected } public sealed class DisconnectInfo { public const int MaxCodeLength = 32; public const int MaxDetailLength = 1024; public string Code { get; } public string Reason { get; } public bool Remote { get; } public bool Retryable { get; } public ConnectionState Phase { get; } public DisconnectInfo(string reason, bool remote = false, string code = "closed", bool retryable = false, ConnectionState phase = ConnectionState.Disconnecting) { if (!Enum.IsDefined(typeof(ConnectionState), phase)) { phase = ConnectionState.Disconnecting; } Code = Bound(code, 32, "closed"); Reason = Bound(reason, 1024, "PeerConnection closed"); Remote = remote; Retryable = retryable; Phase = phase; } private static string Bound(string value, int max, string fallback) { if (string.IsNullOrWhiteSpace(value)) { return fallback; } value = value.Trim(); if (value.Length > max) { return value.Substring(0, max); } return value; } } public class PeerConnection { private readonly object _stateLock = new object(); private ConnectionState _state; private DisconnectInfo _disconnectInfo; public int Id { get; } public ConnectionState State { get { lock (_stateLock) { return _state; } } } internal bool IsDisconnectingOrDisconnected { get { lock (_stateLock) { return _state == ConnectionState.Disconnecting || _state == ConnectionState.Disconnected; } } } internal DisconnectInfo DisconnectReason { get { lock (_stateLock) { return _disconnectInfo; } } } internal PeerConnection(int id) { Id = id; _state = ConnectionState.Handshaking; } internal bool TryTransition(ConnectionState next) { lock (_stateLock) { if (_state == ConnectionState.Disconnected || _state == ConnectionState.Disconnecting) { return false; } if (_state == next) { return true; } if ((_state != ConnectionState.Handshaking || next != ConnectionState.Transferring) && (_state != ConnectionState.Transferring || next != ConnectionState.FullyJoined) && (_state != ConnectionState.Connected || next != ConnectionState.Handshaking) && next != ConnectionState.Disconnecting) { return false; } _state = next; return true; } } internal bool BeginDisconnect(DisconnectInfo info) { if (info == null) { throw new ArgumentNullException("info"); } lock (_stateLock) { if (_state == ConnectionState.Disconnecting || _state == ConnectionState.Disconnected) { return false; } _state = ConnectionState.Disconnecting; _disconnectInfo = info; return true; } } internal bool RecordRemoteDisconnect(DisconnectInfo info) { if (info == null || !info.Remote) { throw new ArgumentException("Remote disconnect info is required", "info"); } lock (_stateLock) { if (_state == ConnectionState.Disconnected) { return false; } if (_disconnectInfo != null) { return false; } _disconnectInfo = info; _state = ConnectionState.Disconnecting; return true; } } internal bool TryMarkDisconnected() { lock (_stateLock) { if (_state == ConnectionState.Disconnected) { return false; } if (_state != ConnectionState.Disconnecting) { return false; } _state = ConnectionState.Disconnected; return true; } } public override string ToString() { return "PeerConnection(" + Id + ")"; } } public sealed class ConnectionEvent { public PeerConnection Connection { get; } public DisconnectInfo Disconnect { get; } public ConnectionEvent(PeerConnection connection, DisconnectInfo disconnect = null) { Connection = connection ?? throw new ArgumentNullException("connection"); Disconnect = disconnect; } } } namespace CardShopCoop.Api { internal interface ICoopBinding { ICoopContext Context { get; } bool PredictionActive { get; } bool IsReconciling { get; } bool IsApplying { get; } void Register(Assembly assembly); Guid Predict(string scope, Action<Guid> send, Action apply, Action undo); void Ack(Guid predictionId); void AckOrApply(Guid predictionId, Action apply); void Confirm(Guid predictionId, Action apply); bool IsPending(Guid predictionId); void Rollback(ICoopContext context, int connectionId, Guid predictionId); } public static class CoopApi { internal static ICoopBinding Binding { get; set; } public static bool IsAvailable => Binding != null; public static ICoopContext Context { get { if (Binding != null) { return Binding.Context; } return null; } } public static ManualLogSource Log { get; internal set; } public static void Register(Assembly assembly) { if (assembly == null) { throw new ArgumentNullException("assembly"); } (Binding ?? throw new InvalidOperationException("CardShopCoopCommunity has not installed its API binding yet; call CoopApi.Register from your plugin's Awake after CardShopCoopCommunity is loaded (declare a BepInDependency on dev.meepen.cardshopcoop, or guard with CoopApi.IsAvailable).")).Register(assembly); } } public abstract class CoopBehaviour : MonoBehaviour { public ICoopContext Context { get; private set; } internal void SetContext(ICoopContext context) { Context = context; } } public interface ICoopMessageRegistry { void RegisterAttributedHandlers(object target); void UnregisterAttributedHandlers(object target); } public interface ICoopContext { bool IsHost { get; } bool IsClient { get; } bool InSession { get; } bool InGame { get; } int LocalConnectionId { get; } int HostConnectionId { get; } IReadOnlyList<int> ConnectionIds { get; } ICoopMessageRegistry Messages { get; } string PeerName(int connectionId); void Send(int connectionId, INetMessage message); void Broadcast(INetMessage message); } public sealed class CoopMessageContext { public PeerConnection Connection { get; } public bool InGame { get; } public int ConnectionId { get { if (Connection != null) { return Connection.Id; } return 0; } } public bool IsAuthenticated { get { if (Connection != null) { if (Connection.State != ConnectionState.Transferring) { return Connection.State == ConnectionState.FullyJoined; } return true; } return false; } } internal CoopMessageContext(PeerConnection connection, bool inGame) { Connection = connection; InGame = inGame; } } public static class CoopLog { private const int SwallowCooldownMs = 2000; private static readonly ConcurrentDictionary<string, int> LastSwallowed = new ConcurrentDictionary<string, int>(); public static void Info(string message) { Safe(delegate(ManualLogSource log) { log.LogInfo((object)message); }); } public static void Warn(string message) { Safe(delegate(ManualLogSource log) { log.LogWarning((object)message); }); } public static void Error(string message) { Safe(delegate(ManualLogSource log) { log.LogError((object)message); }); } public static void Warn(Exception exception) { Safe(delegate(ManualLogSource log) { log.LogWarning((object)("[caught] " + exception)); }); } public static void Error(Exception exception) { Safe(delegate(ManualLogSource log) { log.LogError((object)exception); }); } public static void Swallow(Exception exception, [CallerMemberName] string member = "", [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) { if (exception == null) { return; } string text; try { text = (string.IsNullOrEmpty(file) ? "?" : Path.GetFileNameWithoutExtension(file)); } catch { text = "?"; } string site = text + "." + (member ?? "?") + ":" + line; int tickCount = Environment.TickCount; if (!LastSwallowed.TryGetValue(site, out var value) || tickCount - value >= 2000) { LastSwallowed[site] = tickCount; Safe(delegate(ManualLogSource log) { log.LogWarning((object)("[caught] " + site + ": " + exception)); }); } } private static void Safe(Action<ManualLogSource> write) { ManualLogSource log = CoopApi.Log; if (log == null) { return; } try { write(log); } catch { } } } public interface IPredictedMessage : INetMessage { Guid PredictionId { get; set; } } public static class CoopPredict { public static bool IsAvailable { get { if (CoopApi.Binding != null) { return CoopApi.Binding.PredictionActive; } return false; } } public static bool IsReconciling { get { if (CoopApi.Binding != null) { return CoopApi.Binding.IsReconciling; } return false; } } public static bool IsApplying { get { if (CoopApi.Binding != null) { return CoopApi.Binding.IsApplying; } return false; } } [MethodImpl(MethodImplOptions.NoInlining)] public static Guid Predict(string scope, Action<Guid> send, Action apply, Action undo) { ICoopBinding coopBinding = RequireBinding(); string name = Assembly.GetCallingAssembly().GetName().Name; return coopBinding.Predict(NamespaceScope(scope, name), send, apply, undo); } private static string NamespaceScope(string scope, string caller) { if (string.IsNullOrEmpty(scope)) { throw new ArgumentException("A prediction scope is required.", "scope"); } if (!string.IsNullOrEmpty(caller)) { return caller + ":" + scope; } return scope; } private static ICoopBinding RequireBinding() { ICoopBinding binding = CoopApi.Binding; if (binding == null || !binding.PredictionActive) { throw new InvalidOperationException("Co-op client prediction is not active."); } return binding; } public static void Ack(Guid predictionId) { CoopApi.Binding?.Ack(predictionId); } public static void AckOrApply(Guid predictionId, Action apply) { CoopApi.Binding?.AckOrApply(predictionId, apply); } public static void Confirm(Guid predictionId, Action apply) { CoopApi.Binding?.Confirm(predictionId, apply); } public static bool IsPending(Guid predictionId) { if (CoopApi.Binding != null) { return CoopApi.Binding.IsPending(predictionId); } return false; } public static void Rollback(ICoopContext context, int connectionId, Guid predictionId) { if (!(predictionId == Guid.Empty)) { if (context == null) { throw new ArgumentNullException("context"); } CoopApi.Binding?.Rollback(context, connectionId, predictionId); } } } public static class CoopReflection { private static readonly HashSet<string> Reported = new HashSet<string>(); private static readonly object Gate = new object(); private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public static Type OptionalType(string typeName, string assemblyName) { Type type = ResolveType(typeName, assemblyName); if (type == null) { Report("type", typeName + ", " + assemblyName, required: false); } return type; } public static FieldInfo OptionalField(Type type, string name) { FieldInfo? obj = ((type == null) ? null : type.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); if (obj == null) { Report("field", Describe(type, name), required: false); } return obj; } public static MethodInfo OptionalMethod(Type type, string name, params Type[] args) { MethodInfo methodInfo = ResolveMethod(type, name, args); if (methodInfo == null) { Report("method", Describe(type, name), required: false); } return methodInfo; } public static PropertyInfo OptionalProperty(Type type, string name) { PropertyInfo? obj = ((type == null) ? null : type.GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); if (obj == null) { Report("property", Describe(type, name), required: false); } return obj; } public static FieldInfo RequiredField(Type type, string name) { FieldInfo? obj = ((type == null) ? null : type.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); if (obj == null) { Report("field", Describe(type, name), required: true); } return obj; } public static MethodInfo RequiredMethod(Type type, string name, params Type[] args) { MethodInfo methodInfo = ResolveMethod(type, name, args); if (methodInfo == null) { Report("method", Describe(type, name), required: true); } return methodInfo; } private static Type ResolveType(string typeName, string assemblyName) { try { Type type = Type.GetType(typeName + ", " + assemblyName, throwOnError: false); if (type != null) { return type; } } catch (Exception ex) { ManualLogSource log = CoopApi.Log; if (log != null) { log.LogWarning((object)("[api] type probe failed for " + typeName + ": " + ex.Message)); } } try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type2 = assemblies[i].GetType(typeName, throwOnError: false); if (type2 != null) { return type2; } } } catch (Exception ex2) { ManualLogSource log2 = CoopApi.Log; if (log2 != null) { log2.LogWarning((object)("[api] assembly scan failed for " + typeName + ": " + ex2.Message)); } } return null; } private static MethodInfo ResolveMethod(Type type, string name, Type[] args) { if (type == null) { return null; } if (args == null || args.Length == 0) { MethodInfo methodInfo = null; MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo2 in methods) { if (!(methodInfo2.Name != name)) { if (methodInfo2.GetParameters().Length == 0) { return methodInfo2; } if ((object)methodInfo == null) { methodInfo = methodInfo2; } } } return methodInfo; } return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, args, null); } private static string Describe(Type type, string name) { return ((type == null) ? "<null>" : type.FullName) + "." + name; } private static void Report(string kind, string name, bool required) { string item = (required ? "required:" : "optional:") + kind + ":" + name; lock (Gate) { if (!Reported.Add(item)) { return; } } string text = "coop api: reflection surface changed: " + kind + " " + name + (required ? " (required)" : " (optional; feature disabled)"); if (required) { ManualLogSource log = CoopApi.Log; if (log != null) { log.LogError((object)text); } throw new MissingMemberException(text); } ManualLogSource log2 = CoopApi.Log; if (log2 != null) { log2.LogWarning((object)text); } } } } namespace CardShopCoop.Attributes { [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class ServerBehaviourAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class ClientBehaviourAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class PersistentBehaviourAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class OnClientJoinedAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class OnClientDisconnectedAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class OnFullyJoinedAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class OnSessionStartedAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method, Inherited = false)] public sealed class OnSessionStoppedAttribute : Attribute { } }