Decompiled source of NetKit v0.2.10
plugins/NetKit.Core.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("NetKit.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.10.0")] [assembly: AssemblyInformationalVersion("0.2.10+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("NetKit.Core")] [assembly: AssemblyTitle("NetKit.Core")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace NetKit.Core { public struct AnnounceEntry { public string WireKey; public string Payload; public string Extra; } public sealed class AnnounceBook { private sealed class Entry { public MirrorLatch Latch; public string WireKey = ""; public string LastPayload = ""; public string LastExtra = ""; } private const char KeySep = '\u001f'; private readonly double _refreshSeconds; private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(StringComparer.Ordinal); public int Count => _entries.Count; public AnnounceBook(double refreshSeconds) { _refreshSeconds = refreshSeconds; } public MirrorDecision Decide(string? key, string? payload, string? extra, double now) { string text = RecordKey.Canonical(key); if (text.Length == 0) { return MirrorDecision.None; } return EntryOf(text, key).Latch.Decide((payload == null) ? null : ComposeKey(payload, extra), now); } public void OnLanded(string? key, string payload, string? extra, double now) { string text = RecordKey.Canonical(key); if (text.Length != 0) { Entry entry = EntryOf(text, key); entry.Latch.OnSent(ComposeKey(payload, extra), now); entry.LastPayload = payload ?? ""; entry.LastExtra = extra ?? ""; } } public bool Forget(string? key) { return _entries.Remove(RecordKey.Canonical(key)); } public void Invalidate(string? key) { if (_entries.TryGetValue(RecordKey.Canonical(key), out Entry value)) { value.Latch.Invalidate(); } } public void InvalidateAll() { foreach (KeyValuePair<string, Entry> entry in _entries) { entry.Value.Latch.Invalidate(); } } public void Reset() { _entries.Clear(); } public List<AnnounceEntry> FlushSnapshot() { List<AnnounceEntry> list = new List<AnnounceEntry>(); foreach (KeyValuePair<string, Entry> entry in _entries) { if (entry.Value.Latch.HasSent) { list.Add(new AnnounceEntry { WireKey = entry.Value.WireKey, Payload = entry.Value.LastPayload, Extra = entry.Value.LastExtra }); } } return list; } public bool HasLanded(string? key) { if (_entries.TryGetValue(RecordKey.Canonical(key), out Entry value)) { return value.Latch.HasSent; } return false; } private Entry EntryOf(string canonical, string rawKey) { if (!_entries.TryGetValue(canonical, out Entry value)) { RecordKey.TryParse(rawKey, out string ownerUid, out int slot); Dictionary<string, Entry> entries = _entries; Entry obj = new Entry { Latch = new MirrorLatch(_refreshSeconds), WireKey = RecordKey.Wire(ownerUid, slot) }; value = obj; entries[canonical] = obj; } return value; } private static string ComposeKey(string payload, string? extra) { return payload + "\u001f" + extra; } } public static class BuildStampCompat { public const string Unknown = "unknown"; public static bool IsKnown(string stamp) { if (!string.IsNullOrEmpty(stamp)) { return !string.Equals(stamp, "unknown", StringComparison.OrdinalIgnoreCase); } return false; } public static bool ShouldWarn(string ours, string theirs) { if (IsKnown(ours) && IsKnown(theirs)) { return !string.Equals(ours, theirs, StringComparison.Ordinal); } return false; } } public static class ChannelCompat { public enum Kind { Compatible, ProtoMismatch, VersionMismatch, PeerMissingChannel } public static bool ProtoCompatible(int localProto, int remoteProto) { return localProto == remoteProto; } public static Kind Evaluate(int localProto, int remoteProto, string localVersion, string remoteVersion) { if (localProto != remoteProto) { return Kind.ProtoMismatch; } if (remoteVersion == null) { return Kind.PeerMissingChannel; } if (!string.Equals(localVersion, remoteVersion, StringComparison.Ordinal)) { return Kind.VersionMismatch; } return Kind.Compatible; } public static bool IsReady(Kind kind) { return kind == Kind.Compatible; } } public static class EventFanout { public static void Raise<T>(Action<T> ev, T arg, Action<Delegate, Exception> onError) { if (ev == null) { return; } Delegate[] invocationList = ev.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action<T>)obj)(arg); } catch (Exception arg2) { onError?.Invoke(obj, arg2); } } } public static void ForEach<T>(IEnumerable<T> items, Action<T> body, Action<T, Exception> onError) { if (items == null || body == null) { return; } foreach (T item in items) { try { body(item); } catch (Exception arg) { onError?.Invoke(item, arg); } } } } public sealed class PendingOnce<T> where T : class { private readonly List<T> _items = new List<T>(); public int Count => _items.Count; public bool Add(T item) { if (item == null || _items.Contains(item)) { return false; } _items.Add(item); return true; } public List<T> Drain() { if (_items.Count == 0) { return null; } List<T> result = new List<T>(_items); _items.Clear(); return result; } } public static class ExtensionChange { public static bool ShouldRaiseChanged(bool wasReady, bool hadPrevious, string previousExtension, string newExtension) { if (!wasReady) { return false; } if (!hadPrevious) { return false; } return !string.Equals(previousExtension ?? "", newExtension ?? "", StringComparison.Ordinal); } } public enum FireAndForgetAction { Skip, Drop, Apply, ApplyAndRelay } public readonly struct FireAndForgetDecision { public FireAndForgetAction Action { get; } public string? Reason { get; } public static FireAndForgetDecision Skip => new FireAndForgetDecision(FireAndForgetAction.Skip, null); public static FireAndForgetDecision Apply => new FireAndForgetDecision(FireAndForgetAction.Apply, null); public static FireAndForgetDecision ApplyAndRelay => new FireAndForgetDecision(FireAndForgetAction.ApplyAndRelay, null); private FireAndForgetDecision(FireAndForgetAction action, string? reason) { Action = action; Reason = reason; } public static FireAndForgetDecision Drop(string reason) { return new FireAndForgetDecision(FireAndForgetAction.Drop, reason); } public override string ToString() { if (Reason != null) { return Action.ToString() + "(" + Reason + ")"; } return Action.ToString(); } } public static class FireAndForgetLadder { public const string EmptyIdentity = "empty-identity"; public const string DisabledOnMaster = "disabled-on-master"; public static FireAndForgetDecision OnFire(bool enabled, bool uidEmpty, bool inRoom) { if (!enabled || uidEmpty) { return FireAndForgetDecision.Skip; } if (!inRoom) { return FireAndForgetDecision.Apply; } return FireAndForgetDecision.ApplyAndRelay; } public static FireAndForgetDecision OnCastReceived(bool enabled, bool senderIsMaster, bool senderIsSelf, bool uidEmpty, bool isLocalOwner) { return OnCastReceived(enabled, senderIsMaster, senderIsSelf, uidEmpty, isLocalOwner, masterOriginated: false); } public static FireAndForgetDecision OnCastReceived(bool enabled, bool senderIsMaster, bool senderIsSelf, bool uidEmpty, bool isLocalOwner, bool masterOriginated) { string text = RoleGuard.Evaluate(HandlerRole.SenderMustBeMaster | HandlerRole.SenderMustNotBeSelf, localIsMaster: false, senderIsMaster, senderIsSelf); if (text != null) { return FireAndForgetDecision.Drop(text); } if (!enabled) { return FireAndForgetDecision.Skip; } if (uidEmpty) { return FireAndForgetDecision.Drop("empty-identity"); } if (isLocalOwner && !masterOriginated) { return FireAndForgetDecision.Skip; } return FireAndForgetDecision.Apply; } public static FireAndForgetDecision OnProxyReceived(bool enabled, bool localIsMaster, bool uidEmpty, string? authorizeReason) { string text = RoleGuard.Evaluate(HandlerRole.RunOnMasterOnly, localIsMaster, senderIsMaster: false, senderIsSelf: false); if (text != null) { return FireAndForgetDecision.Drop(text); } if (!enabled) { return FireAndForgetDecision.Drop("disabled-on-master"); } if (uidEmpty) { return FireAndForgetDecision.Drop("empty-identity"); } if (authorizeReason != null) { return FireAndForgetDecision.Drop(authorizeReason); } return FireAndForgetDecision.ApplyAndRelay; } } public static class Heartbeat { public static string Line(string tag, bool isMaster, int peers, int ready, int tx, int rx, int drops, string fragment, double photonTime) { string text = string.Format("[{0}] hb role={1} peers={2} ready={3}/{4}", tag, isMaster ? "M" : "G", peers, ready, peers) + $" pt={photonTime:F1} tx={tx} rx={rx} drops={drops}"; if (!string.IsNullOrEmpty(fragment)) { text = text + " " + fragment; } return text; } } public static class HelloCodec { public struct HelloMsg { public int Proto; public List<ChannelHello> Channels; public string Stamp; public bool SceneReady; } public struct ChannelHello { public string Channel; public string Version; public string Extension; } public const int Version = 1; public static string Encode(HelloMsg m) { List<ChannelHello> list = m.Channels ?? new List<ChannelHello>(); List<string> list2 = new List<string>(2 + list.Count * 3) { NetWire.I(m.Proto), NetWire.I(list.Count) }; foreach (ChannelHello item in list) { list2.Add(NetWire.Esc(item.Channel)); list2.Add(NetWire.Esc(item.Version)); list2.Add(NetWire.Esc(item.Extension)); } list2.Add(NetWire.Esc(m.Stamp ?? "")); list2.Add(m.SceneReady ? "1" : "0"); return NetWire.Join(list2.ToArray()); } public static bool TryDecode(string payload, out HelloMsg m) { m = default(HelloMsg); m.Channels = new List<ChannelHello>(); m.Stamp = ""; List<string> list = NetWire.Split(payload); if (list == null || list.Count < 2) { return false; } if (!NetWire.TryI(list[0], out m.Proto) || m.Proto <= 0) { return false; } if (!NetWire.TryI(list[1], out var v) || v < 0) { return false; } if (list.Count < 2 + v * 3) { return false; } for (int i = 0; i < v; i++) { int num = 2 + i * 3; string text = list[num]; if (string.IsNullOrEmpty(text)) { return false; } m.Channels.Add(new ChannelHello { Channel = text, Version = list[num + 1], Extension = list[num + 2] }); } int num2 = 2 + v * 3; if (list.Count > num2) { m.Stamp = list[num2]; } m.SceneReady = true; if (list.Count > num2 + 1) { m.SceneReady = list[num2 + 1] != "0"; } return true; } public static string? VersionOf(in HelloMsg m, string channel) { if (m.Channels == null) { return null; } foreach (ChannelHello channel2 in m.Channels) { if (string.Equals(channel2.Channel, channel, StringComparison.Ordinal)) { return channel2.Version; } } return null; } public static string? ExtensionOf(in HelloMsg m, string channel) { if (m.Channels == null) { return null; } foreach (ChannelHello channel2 in m.Channels) { if (string.Equals(channel2.Channel, channel, StringComparison.Ordinal)) { return channel2.Extension; } } return null; } } public sealed class HelloResend { private readonly double _intervalSeconds; private string? _broadcast; private readonly Dictionary<int, string> _targeted = new Dictionary<int, string>(); private double _nextCheck; public bool HasBaseline { get { if (_broadcast == null) { return _targeted.Count > 0; } return true; } } public int TrackedActorCount => _targeted.Count; public HelloResend(double intervalSeconds) { _intervalSeconds = intervalSeconds; } public void RecordSent(string payload) { _broadcast = payload ?? ""; _targeted.Clear(); } public void RecordSentTo(int actor, string payload) { _targeted[actor] = payload ?? ""; } public void ForgetActor(int actor) { _targeted.Remove(actor); } public bool DueForCheck(double now) { if (_intervalSeconds <= 0.0) { return false; } if (!HasBaseline) { return false; } if (now < _nextCheck) { return false; } _nextCheck = now + _intervalSeconds; return true; } public bool PayloadChanged(string currentPayload) { string b = currentPayload ?? ""; if (_broadcast != null && !string.Equals(_broadcast, b, StringComparison.Ordinal)) { return true; } foreach (KeyValuePair<int, string> item in _targeted) { if (!string.Equals(item.Value, b, StringComparison.Ordinal)) { return true; } } return false; } public bool ShouldResend(double now, string currentPayload) { if (DueForCheck(now)) { return PayloadChanged(currentPayload); } return false; } public void Reset() { _broadcast = null; _targeted.Clear(); _nextCheck = 0.0; } } public enum MirrorDecision { None, SendChanged, SendPeriodic, Clear } public sealed class MirrorLatch { private readonly double _resendSeconds; private readonly bool _clearOnNull; private string _lastKey; private bool _invalidated; private double _resendAt; public bool HasSent => _lastKey != null; public string LastKey => _lastKey; public MirrorLatch(double resendSeconds = 0.0, bool clearOnNull = false) { _resendSeconds = resendSeconds; _clearOnNull = clearOnNull; } public MirrorDecision Decide(string key, double now) { if (key == null) { if (!_clearOnNull || !HasSent) { return MirrorDecision.None; } return MirrorDecision.Clear; } if (_invalidated || _lastKey == null || !string.Equals(key, _lastKey, StringComparison.Ordinal)) { return MirrorDecision.SendChanged; } if (_resendSeconds > 0.0 && now >= _resendAt) { return MirrorDecision.SendPeriodic; } return MirrorDecision.None; } public void OnSent(string key, double now) { _lastKey = key; _invalidated = false; _resendAt = now + _resendSeconds; } public void OnCleared() { _lastKey = null; _invalidated = false; } public void Invalidate() { _invalidated = true; } public void Reset() { _lastKey = null; _invalidated = false; } } public sealed class NetCounters { private sealed class VerbTrace { public int Sends; public int Recvs; public int Drops; public double LastSendAt = -1.0; public double LastRecvAt = -1.0; public double LastDropAt = -1.0; public readonly Dictionary<string, int> DropReasons = new Dictionary<string, int>(); } private readonly Dictionary<string, VerbTrace> _trace = new Dictionary<string, VerbTrace>(); private readonly int _verbCap; private readonly int _reasonCap; private int _overflowEvents; public const int DefaultVerbCap = 64; public const int DefaultReasonCap = 32; public bool Empty { get { if (_trace.Count == 0) { return _overflowEvents == 0; } return false; } } public int OverflowEvents => _overflowEvents; public int TotalSends { get { int num = 0; foreach (VerbTrace value in _trace.Values) { num += value.Sends; } return num; } } public int TotalRecvs { get { int num = 0; foreach (VerbTrace value in _trace.Values) { num += value.Recvs; } return num; } } public int TotalDrops { get { int num = 0; foreach (VerbTrace value in _trace.Values) { num += value.Drops; } return num; } } public NetCounters() : this(64) { } public NetCounters(int verbCap, int reasonCap = 32) { _verbCap = ((verbCap < 1) ? 1 : verbCap); _reasonCap = ((reasonCap < 1) ? 1 : reasonCap); } private VerbTrace For(string verb) { verb = verb ?? "?"; if (_trace.TryGetValue(verb, out VerbTrace value)) { return value; } if (_trace.Count >= _verbCap) { _overflowEvents++; return null; } return _trace[verb] = new VerbTrace(); } public void RecordSend(string verb, double now) { VerbTrace verbTrace = For(verb); if (verbTrace != null) { verbTrace.Sends++; verbTrace.LastSendAt = now; } } public void RecordRecv(string verb, double now) { VerbTrace verbTrace = For(verb); if (verbTrace != null) { verbTrace.Recvs++; verbTrace.LastRecvAt = now; } } public void RecordDrop(string verb, string reason, double now) { VerbTrace verbTrace = For(verb); if (verbTrace != null) { verbTrace.Drops++; verbTrace.LastDropAt = now; reason = reason ?? "?"; if (verbTrace.DropReasons.TryGetValue(reason, out var value)) { verbTrace.DropReasons[reason] = value + 1; } else if (verbTrace.DropReasons.Count >= _reasonCap) { _overflowEvents++; } else { verbTrace.DropReasons[reason] = 1; } } } public string Summary(string tag, double now) { if (_trace.Count == 0) { if (_overflowEvents != 0) { return $"[{tag}] no traffic recorded ({_overflowEvents} event(s) refused by the counter caps)."; } return "[" + tag + "] no traffic recorded."; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append($"[{tag}] traffic (t={now:F0}s):"); List<string> list = new List<string>(_trace.Keys); list.Sort(StringComparer.Ordinal); foreach (string item in list) { VerbTrace verbTrace = _trace[item]; string arg = ""; if (verbTrace.Drops > 0) { List<string> list2 = new List<string>(); foreach (KeyValuePair<string, int> dropReason in verbTrace.DropReasons) { list2.Add($"{dropReason.Key}×{dropReason.Value}"); } list2.Sort(StringComparer.Ordinal); arg = string.Format(" drop={0} ({1}) lastDrop={2:F0}s", verbTrace.Drops, string.Join(", ", list2.ToArray()), verbTrace.LastDropAt); } stringBuilder.Append(string.Format("\n[{0}] {1}: send={2}{3}", tag, item, verbTrace.Sends, (verbTrace.Sends > 0) ? $" lastSend={verbTrace.LastSendAt:F0}s" : "") + string.Format(" recv={0}{1}{2}", verbTrace.Recvs, (verbTrace.Recvs > 0) ? $" lastRecv={verbTrace.LastRecvAt:F0}s" : "", arg)); } if (_overflowEvents > 0) { stringBuilder.Append($"\n[{tag}] (+{_overflowEvents} event(s) refused by the counter caps: " + $"verbCap={_verbCap} reasonCap={_reasonCap} — a channel legitimately has a handful " + "of constant verbs, so hitting this means something is inventing them.)"); } return stringBuilder.ToString(); } } internal static class NetWire { public static string I(int v) { return v.ToString(CultureInfo.InvariantCulture); } public static string F(float v) { return v.ToString("R", CultureInfo.InvariantCulture); } public static bool TryI(string s, out int v) { return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v); } public static bool TryF(string s, out float v) { return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v); } public static string Join(params string[] fields) { return string.Join(";", fields); } public static string Esc(string s) { if (string.IsNullOrEmpty(s)) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length + 4); foreach (char c in s) { switch (c) { case '\\': stringBuilder.Append("\\\\"); break; case ';': stringBuilder.Append("\\s"); break; case '\n': stringBuilder.Append("\\n"); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } public static List<string>? Split(string payload) { if (payload == null) { return null; } List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < payload.Length; i++) { char c = payload[i]; switch (c) { case '\\': if (i + 1 >= payload.Length) { return null; } switch (payload[++i]) { case '\\': stringBuilder.Append('\\'); break; case 's': stringBuilder.Append(';'); break; case 'n': stringBuilder.Append('\n'); break; default: return null; } break; case ';': list.Add(stringBuilder.ToString()); stringBuilder.Length = 0; break; default: stringBuilder.Append(c); break; } } list.Add(stringBuilder.ToString()); return list; } } public sealed class NoHandlerDrops { public readonly struct Entry { public string Channel { get; } public string Verb { get; } public int Count { get; } public Entry(string channel, string verb, int count) { Channel = channel; Verb = verb; Count = count; } } private readonly Dictionary<string, Dictionary<string, int>> _byChannel = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal); public bool Empty { get { foreach (Dictionary<string, int> value in _byChannel.Values) { if (value.Count > 0) { return false; } } return true; } } public bool Record(string channel, string verb) { channel = channel ?? "?"; verb = verb ?? "?"; if (!_byChannel.TryGetValue(channel, out Dictionary<string, int> value)) { value = (_byChannel[channel] = new Dictionary<string, int>(StringComparer.Ordinal)); } bool result = !value.ContainsKey(verb); value.TryGetValue(verb, out var value2); value[verb] = value2 + 1; return result; } public int Count(string channel, string verb) { if (channel != null && verb != null && _byChannel.TryGetValue(channel, out Dictionary<string, int> value) && value.TryGetValue(verb, out var value2)) { return value2; } return 0; } public IEnumerable<Entry> NonZero() { List<string> list = new List<string>(_byChannel.Keys); list.Sort(StringComparer.Ordinal); foreach (string c in list) { List<string> list2 = new List<string>(_byChannel[c].Keys); list2.Sort(StringComparer.Ordinal); foreach (string item in list2) { yield return new Entry(c, item, _byChannel[c][item]); } } } public string Summary(string tag) { List<string> list = new List<string>(); foreach (Entry item in NonZero()) { list.Add($"{item.Channel}.{item.Verb}×{item.Count}"); } if (list.Count != 0) { return "[" + tag + "] droppedVerbs: " + string.Join(", ", list.ToArray()); } return "[" + tag + "] droppedVerbs: none"; } } public static class PayloadSizeGuard { public const int PunPracticalLimitBytes = 32717; public const int WarnThresholdBytes = 26173; public static int Utf8ByteSize(string payload) { if (!string.IsNullOrEmpty(payload)) { return Encoding.UTF8.GetByteCount(payload); } return 0; } public static bool ShouldWarn(int utf8ByteSize) { return utf8ByteSize >= 26173; } } public sealed class PayloadSizeWarnLedger { private readonly HashSet<string> _warned = new HashSet<string>(StringComparer.Ordinal); public static string Key(string channelId, string verb) { return channelId + "|" + verb; } public bool ShouldWarn(string channelId, string verb) { return _warned.Add(Key(channelId, verb)); } public void Clear() { _warned.Clear(); } } public sealed class PeerHelloStore { public struct Entry { public int Actor; public HelloCodec.HelloMsg Hello; public bool IsMaster; } public struct ChannelReadiness { public int Actor; public bool Ready; public ChannelCompat.Kind Kind; public string Version; public string Extension; public bool IsMaster; } private readonly Dictionary<int, Entry> _entries = new Dictionary<int, Entry>(); public int Count => _entries.Count; public IEnumerable<Entry> Entries => _entries.Values; public void Record(int actor, HelloCodec.HelloMsg hello, bool isMaster) { _entries[actor] = new Entry { Actor = actor, Hello = hello, IsMaster = isMaster }; } public bool TryGet(int actor, out Entry entry) { return _entries.TryGetValue(actor, out entry); } public void Remove(int actor) { _entries.Remove(actor); } public void Clear() { _entries.Clear(); } public List<ChannelReadiness> EvaluateChannel(int localProto, string channelId, string localVersion) { List<ChannelReadiness> list = new List<ChannelReadiness>(_entries.Count); foreach (Entry value in _entries.Values) { Entry current = value; string text = HelloCodec.VersionOf(in current.Hello, channelId); ChannelCompat.Kind kind = ChannelCompat.Evaluate(localProto, current.Hello.Proto, localVersion, text); list.Add(new ChannelReadiness { Actor = current.Actor, Ready = ChannelCompat.IsReady(kind), Kind = kind, Version = (text ?? ""), Extension = (HelloCodec.ExtensionOf(in current.Hello, channelId) ?? ""), IsMaster = current.IsMaster }); } return list; } } public sealed class PeerLedger { private readonly Dictionary<int, double> _expectedSince = new Dictionary<int, double>(); private readonly HashSet<int> _helloed = new HashSet<int>(); public int PendingCount => _expectedSince.Count; public int HelloedCount => _helloed.Count; public IReadOnlyDictionary<int, double> Pending => _expectedSince; public bool HasHello(int actor) { return _helloed.Contains(actor); } public bool Expect(int actor, double now) { if (_helloed.Contains(actor)) { return false; } if (_expectedSince.ContainsKey(actor)) { return false; } _expectedSince[actor] = now; return true; } public void MarkHello(int actor) { _expectedSince.Remove(actor); _helloed.Add(actor); } public List<int>? DueForWarning(double now, double warnSeconds) { List<int> list = null; if (_expectedSince.Count == 0) { return null; } foreach (KeyValuePair<int, double> item in _expectedSince) { if (now - item.Value > warnSeconds) { (list ?? (list = new List<int>())).Add(item.Key); } } if (list != null) { foreach (int item2 in list) { _expectedSince.Remove(item2); } } return list; } public double GraceElapsed(int actor, double now) { if (!_expectedSince.TryGetValue(actor, out var value)) { return -1.0; } return now - value; } public void Reset() { _expectedSince.Clear(); _helloed.Clear(); } } public static class PunLog { public static readonly string[] Signatures = new string[3] { "Received OnSerialization for view ID", "has no method", "PhotonView ID duplicate found" }; public static int Match(string condition) { if (string.IsNullOrEmpty(condition)) { return -1; } for (int i = 0; i < Signatures.Length; i++) { if (condition.IndexOf(Signatures[i], StringComparison.Ordinal) >= 0) { return i; } } return -1; } public static bool TryParseViewId(string condition, out int id) { id = 0; if (string.IsNullOrEmpty(condition)) { return false; } int num = condition.IndexOf(Signatures[0], StringComparison.Ordinal); if (num < 0) { return false; } int i; for (i = num + Signatures[0].Length; i < condition.Length && !char.IsDigit(condition[i]); i++) { } int j; for (j = i; j < condition.Length && char.IsDigit(condition[j]); j++) { } if (j <= i) { return false; } return int.TryParse(condition.Substring(i, j - i), out id); } } public sealed class PunSignatureCounters { private readonly int[] _counts = new int[PunLog.Signatures.Length]; private readonly double[] _firstAt; private readonly double[] _lastAt; public int UnknownViewCount => _counts[0]; public PunSignatureCounters() { _firstAt = new double[PunLog.Signatures.Length]; _lastAt = new double[PunLog.Signatures.Length]; for (int i = 0; i < _firstAt.Length; i++) { _firstAt[i] = -1.0; _lastAt[i] = -1.0; } } public void Record(int signatureIndex, double now) { if (signatureIndex >= 0 && signatureIndex < _counts.Length) { _counts[signatureIndex]++; if (_firstAt[signatureIndex] < 0.0) { _firstAt[signatureIndex] = now; } _lastAt[signatureIndex] = now; } } public int Count(int signatureIndex) { if (signatureIndex < 0 || signatureIndex >= _counts.Length) { return 0; } return _counts[signatureIndex]; } public string Summary(string tag) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < PunLog.Signatures.Length; i++) { if (_counts[i] > 0) { stringBuilder.Append($"\n[{tag}] PUN-signature '{PunLog.Signatures[i]}': {_counts[i]}× " + $"(first {_firstAt[i]:F0}s, last {_lastAt[i]:F0}s)"); } } if (stringBuilder.Length != 0) { return stringBuilder.ToString(1, stringBuilder.Length - 1); } return ""; } } public sealed class UnknownViewTable { private sealed class Row { public int Count; public double FirstAt; public double LastAt; public bool RunawayAnnounced; } private readonly Dictionary<int, Row> _rows = new Dictionary<int, Row>(); private readonly int _cap; private readonly int _runawayThreshold; public const int DefaultRunawayThreshold = 500; public int IdCount => _rows.Count; public UnknownViewTable(int cap = 64, int runawayThreshold = 500) { _cap = ((cap < 1) ? 1 : cap); _runawayThreshold = ((runawayThreshold < 1) ? 1 : runawayThreshold); } public bool Record(int id, double now) { bool runaway; return Record(id, now, out runaway); } public bool Record(int id, double now, out bool runaway) { runaway = false; if (_rows.TryGetValue(id, out Row value)) { value.Count++; value.LastAt = now; if (!value.RunawayAnnounced && value.Count >= _runawayThreshold) { value.RunawayAnnounced = true; runaway = true; } return false; } if (_rows.Count >= _cap) { return false; } _rows[id] = new Row { Count = 1, FirstAt = now, LastAt = now }; return true; } public int CountOf(int id) { if (!_rows.TryGetValue(id, out Row value)) { return 0; } return value.Count; } public string TopSummary(int max = 4) { if (_rows.Count == 0 || max < 1) { return ""; } List<KeyValuePair<int, Row>> list = new List<KeyValuePair<int, Row>>(_rows); list.Sort((KeyValuePair<int, Row> a, KeyValuePair<int, Row> b) => b.Value.Count.CompareTo(a.Value.Count)); StringBuilder stringBuilder = new StringBuilder(); int num = ((list.Count < max) ? list.Count : max); for (int num2 = 0; num2 < num; num2++) { if (num2 > 0) { stringBuilder.Append(' '); } stringBuilder.Append(list[num2].Key).Append('×').Append(list[num2].Value.Count); } return stringBuilder.ToString(); } public string Format(string tag) { if (_rows.Count == 0) { return "[" + tag + "] unknown-view table: empty."; } StringBuilder stringBuilder = new StringBuilder($"[{tag}] unknown-view table ({_rows.Count} id(s)):"); foreach (KeyValuePair<int, Row> row in _rows) { stringBuilder.Append($"\n[{tag}] id={row.Key}: {row.Value.Count}× (first {row.Value.FirstAt:F0}s, last {row.Value.LastAt:F0}s)"); } return stringBuilder.ToString(); } } public sealed class QuietVerbSet { public static readonly QuietVerbSet Empty = new QuietVerbSet(null); private readonly HashSet<string> _verbs; public int Count { get { if (_verbs == null) { return 0; } return _verbs.Count; } } public QuietVerbSet(IEnumerable<string> verbs) { if (verbs == null) { return; } foreach (string verb in verbs) { if (!string.IsNullOrEmpty(verb)) { (_verbs ?? (_verbs = new HashSet<string>(StringComparer.Ordinal))).Add(verb); } } } public bool IsQuiet(string verb) { if (_verbs != null && verb != null) { return _verbs.Contains(verb); } return false; } } public static class RecordKey { public const char Separator = ':'; private const char ExtraSep = '\u001f'; public static bool TryParse(string? wireKey, out string ownerUid, out int slot) { ownerUid = wireKey ?? ""; slot = 0; if (string.IsNullOrEmpty(wireKey)) { return false; } int num = wireKey.LastIndexOf(':'); if (num > 0 && num < wireKey.Length - 1 && int.TryParse(wireKey.Substring(num + 1), NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { ownerUid = wireKey.Substring(0, num); slot = result; } return ownerUid.Length > 0; } public static string Canonical(string? wireKey) { if (!TryParse(wireKey, out string ownerUid, out int slot)) { return ""; } return Compose(ownerUid, slot); } public static string Compose(string ownerUid, int slot) { return ownerUid + ":" + ((slot >= 0) ? slot : 0).ToString(CultureInfo.InvariantCulture); } public static string Wire(string ownerUid, int slot) { if (slot > 0) { return ownerUid + ":" + slot.ToString(CultureInfo.InvariantCulture); } return ownerUid; } public static string EncodeExtraSlot(string wireKey, string? extra) { if (!string.IsNullOrEmpty(extra)) { return wireKey + "\u001f" + extra; } return wireKey; } public static void DecodeExtraSlot(string? slotValue, out string wireKey, out string extra) { string text = slotValue ?? ""; int num = text.IndexOf('\u001f'); if (num < 0) { wireKey = text; extra = ""; } else { wireKey = text.Substring(0, num); extra = text.Substring(num + 1); } } } public enum OwnerResolution { Unknown, OwnedBySender, NotOwnedBySender } public enum RebindRule { OnlyIfPrevActorGone } public enum RecordApply { RefusedEmptyKey, DeferredOwnerUnknown, RefusedNotOwner, RefusedRebindLive, Created, Updated, Refreshed, Rebound } public static class StoreReleaseWire { public const string OwnerReleasePrefix = "owner release: "; public const string DefaultReason = "released by owner"; } public enum RecordRelease { NoRow, RefusedWrongSender, Cleared } public enum RecordTickStep { Keep, ClearOwnerMissing } public sealed class RecordRow { public string Key { get; internal set; } = ""; public string OwnerUid { get; internal set; } = ""; public int Slot { get; internal set; } public int ActorId { get; internal set; } public string Payload { get; internal set; } = ""; public string Extra { get; internal set; } = ""; public double LastSetAt { get; internal set; } public double OwnerMissingSince { get; internal set; } } public sealed class RecordTable { private readonly Dictionary<string, RecordRow> _rows = new Dictionary<string, RecordRow>(StringComparer.Ordinal); public int Count => _rows.Count; public bool TryGet(string? wireKey, out RecordRow row) { return _rows.TryGetValue(RecordKey.Canonical(wireKey), out row); } public List<RecordRow> RowsSnapshot() { List<RecordRow> list = new List<RecordRow>(_rows.Count); foreach (KeyValuePair<string, RecordRow> row in _rows) { list.Add(row.Value); } return list; } public RecordApply Apply(string? wireKey, string? payload, string? extra, int senderActor, double now, Func<int, bool>? actorPresent, Func<string, int, int, OwnerResolution>? resolveOwner, out RecordRow? row) { row = null; if (!RecordKey.TryParse(wireKey, out string ownerUid, out int slot)) { return RecordApply.RefusedEmptyKey; } string key = RecordKey.Compose(ownerUid, slot); payload = payload ?? ""; extra = extra ?? ""; if (!_rows.TryGetValue(key, out RecordRow value)) { switch (Resolve(resolveOwner, ownerUid, slot, senderActor)) { case OwnerResolution.Unknown: return RecordApply.DeferredOwnerUnknown; case OwnerResolution.NotOwnedBySender: return RecordApply.RefusedNotOwner; default: row = new RecordRow { Key = key, OwnerUid = ownerUid, Slot = slot, ActorId = senderActor }; Stamp(row, payload, extra, now); _rows[key] = row; return RecordApply.Created; } } if (value.ActorId != senderActor) { if (actorPresent == null || actorPresent(value.ActorId)) { return RecordApply.RefusedRebindLive; } switch (Resolve(resolveOwner, ownerUid, slot, senderActor)) { case OwnerResolution.Unknown: return RecordApply.DeferredOwnerUnknown; case OwnerResolution.NotOwnedBySender: return RecordApply.RefusedNotOwner; default: value.ActorId = senderActor; value.OwnerMissingSince = 0.0; Stamp(value, payload, extra, now); row = value; return RecordApply.Rebound; } } bool num = !string.Equals(value.Payload, payload, StringComparison.Ordinal) || !string.Equals(value.Extra, extra, StringComparison.Ordinal); Stamp(value, payload, extra, now); row = value; if (!num) { return RecordApply.Refreshed; } return RecordApply.Updated; } private static OwnerResolution Resolve(Func<string, int, int, OwnerResolution>? resolveOwner, string uid, int slot, int senderActor) { return resolveOwner?.Invoke(uid, slot, senderActor) ?? OwnerResolution.OwnedBySender; } private static void Stamp(RecordRow row, string payload, string extra, double now) { row.Payload = payload; row.Extra = extra; row.LastSetAt = now; } public RecordRelease Release(string? wireKey, int senderActor, out RecordRow? row) { row = null; string key = RecordKey.Canonical(wireKey); if (!_rows.TryGetValue(key, out RecordRow value)) { return RecordRelease.NoRow; } if (value.ActorId != senderActor) { return RecordRelease.RefusedWrongSender; } _rows.Remove(key); row = value; return RecordRelease.Cleared; } public bool Clear(string? wireKey, out RecordRow? row) { string key = RecordKey.Canonical(wireKey); if (!_rows.TryGetValue(key, out RecordRow value)) { row = null; return false; } _rows.Remove(key); row = value; return true; } public List<RecordRow> ClearActor(int actorId) { List<RecordRow> list = new List<RecordRow>(); foreach (KeyValuePair<string, RecordRow> row in _rows) { if (row.Value.ActorId == actorId) { list.Add(row.Value); } } foreach (RecordRow item in list) { _rows.Remove(item.Key); } return list; } public List<RecordRow> ClearAll() { List<RecordRow> result = RowsSnapshot(); _rows.Clear(); return result; } public RecordTickStep TickRow(RecordRow row, bool ownerResolvable, double now, double ownerMissingSeconds) { if (row == null) { return RecordTickStep.Keep; } if (ownerResolvable) { row.OwnerMissingSince = 0.0; return RecordTickStep.Keep; } if (row.OwnerMissingSince <= 0.0) { row.OwnerMissingSince = now; return RecordTickStep.Keep; } if (!(now - row.OwnerMissingSince > ownerMissingSeconds)) { return RecordTickStep.Keep; } return RecordTickStep.ClearOwnerMissing; } } public sealed class RequestBook { public struct Expired { public string Verb; public string Token; } private sealed class Row { public string Verb; public double Deadline; } public const string RefusedInFlight = "in-flight"; private readonly Dictionary<string, Row> _byToken = new Dictionary<string, Row>(StringComparer.Ordinal); private int _counter; public int PendingCount => _byToken.Count; public string Begin(string verb, int actor, double now, double timeoutSeconds, bool singleFlight, out string refusal) { refusal = null; if (singleFlight && HasPending(verb)) { refusal = "in-flight"; return null; } string text = actor.ToString(CultureInfo.InvariantCulture) + "-" + (++_counter).ToString(CultureInfo.InvariantCulture); _byToken[text] = new Row { Verb = verb, Deadline = now + timeoutSeconds }; return text; } public void Abort(string token) { if (token != null) { _byToken.Remove(token); } } public bool TryResolve(string token, out string verb) { verb = null; if (token == null || !_byToken.TryGetValue(token, out Row value)) { return false; } verb = value.Verb; _byToken.Remove(token); return true; } public List<Expired> TakeExpired(double now) { List<Expired> list = null; List<string> list2 = null; foreach (KeyValuePair<string, Row> item in _byToken) { if (!(item.Value.Deadline > now)) { (list ?? (list = new List<Expired>())).Add(new Expired { Verb = item.Value.Verb, Token = item.Key }); (list2 ?? (list2 = new List<string>())).Add(item.Key); } } if (list2 != null) { foreach (string item2 in list2) { _byToken.Remove(item2); } } return list; } public List<Expired> TakeAll() { if (_byToken.Count == 0) { return null; } List<Expired> list = new List<Expired>(_byToken.Count); foreach (KeyValuePair<string, Row> item in _byToken) { list.Add(new Expired { Verb = item.Value.Verb, Token = item.Key }); } _byToken.Clear(); return list; } public bool HasPending(string verb) { foreach (KeyValuePair<string, Row> item in _byToken) { if (string.Equals(item.Value.Verb, verb, StringComparison.Ordinal)) { return true; } } return false; } public bool TryGetPending(string verb, out string token, out double deadline) { foreach (KeyValuePair<string, Row> item in _byToken) { if (string.Equals(item.Value.Verb, verb, StringComparison.Ordinal)) { token = item.Key; deadline = item.Value.Deadline; return true; } } token = null; deadline = 0.0; return false; } } public static class RequestCodec { public static string Sanitize(string s) { if (!string.IsNullOrEmpty(s)) { return s.Replace('\t', ' '); } return ""; } public static string EncodeRequest(string token, string payload) { if (!string.IsNullOrEmpty(payload)) { return Sanitize(token) + "\t" + payload; } return Sanitize(token); } public static bool TryParseRequest(string wire, out string token, out string payload) { token = ""; payload = ""; if (string.IsNullOrEmpty(wire)) { return false; } int num = wire.IndexOf('\t'); if (num < 0) { token = wire; return true; } if (num == 0) { return false; } token = wire.Substring(0, num); payload = wire.Substring(num + 1); return true; } public static string EncodeAck(string token, string result) { return Sanitize(token) + "\t" + Sanitize(result); } public static bool TryParseAck(string wire, out string token, out string result) { token = ""; result = ""; if (string.IsNullOrEmpty(wire)) { return false; } int num = wire.IndexOf('\t'); if (num <= 0 || num == wire.Length - 1) { return false; } string text = wire.Substring(num + 1); if (text.IndexOf('\t') >= 0) { return false; } token = wire.Substring(0, num); result = text; return true; } } [Flags] public enum HandlerRole { Any = 0, RunOnMasterOnly = 1, RunOnGuestOnly = 2, SenderMustBeMaster = 4, SenderMustNotBeSelf = 8 } public static class RoleGuard { public const string NotMasterMachine = "role:not-master-machine"; public const string NotGuestMachine = "role:not-guest-machine"; public const string SenderNotMaster = "role:sender-not-master"; public const string SenderIsSelf = "role:sender-is-self"; public static string? Evaluate(HandlerRole role, bool localIsMaster, bool senderIsMaster, bool senderIsSelf) { if ((role & HandlerRole.RunOnMasterOnly) != HandlerRole.Any && !localIsMaster) { return "role:not-master-machine"; } if ((role & HandlerRole.RunOnGuestOnly) != 0 && localIsMaster) { return "role:not-guest-machine"; } if ((role & HandlerRole.SenderMustBeMaster) != HandlerRole.Any && !senderIsMaster) { return "role:sender-not-master"; } if ((role & HandlerRole.SenderMustNotBeSelf) != 0 && senderIsSelf) { return "role:sender-is-self"; } return null; } } public struct RoomChange { public string? OldRoomName; public string? NewRoomName; } public sealed class RoomWatch { private bool _inRoom; private string? _room; public bool Update(bool inRoom, string? roomName, out RoomChange change) { string text = (inRoom ? (roomName ?? "") : null); string text2 = (_inRoom ? (_room ?? "") : null); if (string.Equals(text, text2, StringComparison.Ordinal)) { change = default(RoomChange); return false; } change = new RoomChange { OldRoomName = text2, NewRoomName = text }; _inRoom = inRoom; _room = roomName; return true; } } public sealed class SceneReadyGate { private string? _firedScene; public bool Fire(bool inRoom, bool ready, string? scene) { if (!inRoom) { _firedScene = null; return false; } if (!ready || string.IsNullOrEmpty(scene)) { return false; } if (string.Equals(scene, _firedScene, StringComparison.Ordinal)) { return false; } _firedScene = scene; return true; } public void Reset() { _firedScene = null; } } public static class OutOfRoomReset { public static bool ShouldReset(int pendingCount, int helloedCount, int peerCount, int channelReadyCount) { if (pendingCount <= 0 && helloedCount <= 0 && peerCount <= 0) { return channelReadyCount > 0; } return true; } } public struct SimEnvelope { public string Channel; public string Verb; public int Seq; public string Extra; public string Payload; } public sealed class SimLink { private struct Entry { public SimEnvelope Env; public double Due; public long Order; } private readonly List<Entry> _queue = new List<Entry>(); private readonly Random _rng; private readonly int _delayMs; private readonly int _jitterMs; private readonly int _dropPct; private readonly bool _ordered; private double _lastDue; private long _order; public int Enqueued { get; private set; } public int Dropped { get; private set; } public int Delivered { get; private set; } public int PendingCount => _queue.Count; public SimLink(int delayMs, int jitterMs, int dropPct, bool ordered, int seed) { _delayMs = ((delayMs >= 0) ? delayMs : 0); _jitterMs = ((jitterMs >= 0) ? jitterMs : 0); _dropPct = ((dropPct >= 0) ? ((dropPct > 100) ? 100 : dropPct) : 0); _ordered = ordered; _rng = new Random(seed); } public bool Enqueue(SimEnvelope env, double now) { if (_dropPct > 0 && _rng.Next(100) < _dropPct) { Dropped++; return false; } double num = now + (double)(_delayMs + ((_jitterMs > 0) ? _rng.Next(_jitterMs + 1) : 0)) / 1000.0; if (_ordered && num < _lastDue) { num = _lastDue; } _lastDue = (_ordered ? num : Math.Max(_lastDue, num)); _queue.Add(new Entry { Env = env, Due = num, Order = _order++ }); Enqueued++; return true; } public List<SimEnvelope> DrainDue(double now) { List<Entry> list = null; for (int num = _queue.Count - 1; num >= 0; num--) { if (!(_queue[num].Due > now)) { List<Entry> obj = list ?? new List<Entry>(); list = obj; obj.Add(_queue[num]); _queue.RemoveAt(num); } } if (list == null) { return null; } list.Sort((Entry a, Entry b) => (a.Due == b.Due) ? a.Order.CompareTo(b.Order) : a.Due.CompareTo(b.Due)); List<SimEnvelope> list2 = new List<SimEnvelope>(list.Count); foreach (Entry item in list) { list2.Add(item.Env); } Delivered += list2.Count; return list2; } public void Clear() { _queue.Clear(); _lastDue = 0.0; } public string Describe() { return $"delay={_delayMs}ms jitter={_jitterMs}ms drop={_dropPct}% " + string.Format("{0} queued={1} dropped={2} ", _ordered ? "ordered" : "unordered", Enqueued, Dropped) + $"delivered={Delivered} pending={_queue.Count}"; } } public enum StoreAuthorityKind { Master, Owner, PeerOwned } public static class StoreRoles { public static HandlerRole For(StoreAuthorityKind kind) { return kind switch { StoreAuthorityKind.Master => HandlerRole.RunOnMasterOnly, StoreAuthorityKind.PeerOwned => HandlerRole.SenderMustNotBeSelf, _ => HandlerRole.SenderMustBeMaster | HandlerRole.SenderMustNotBeSelf, }; } public static bool KeyFromSender(StoreAuthorityKind kind) { return kind == StoreAuthorityKind.PeerOwned; } public static bool BroadcastsToOthers(StoreAuthorityKind kind) { if (kind != StoreAuthorityKind.Owner) { return kind == StoreAuthorityKind.PeerOwned; } return true; } public static bool OwnerBindingApplies(StoreAuthorityKind kind) { return kind == StoreAuthorityKind.Master; } } public static class TimeoutConfig { public const int Vanilla = 10000; public static int Resolve(int configured, out bool negativeWasInvalid) { negativeWasInvalid = configured < 0; if (configured <= 0) { return 0; } return configured; } public static bool Armed(int configured) { bool negativeWasInvalid; return Resolve(configured, out negativeWasInvalid) > 0; } } public sealed class TraceRing { public struct Entry { public double T; public string Line; } private readonly Entry[] _ring; private int _next; public int Capacity => _ring.Length; public TraceRing(int capacity = 32) { if (capacity < 1) { capacity = 1; } _ring = new Entry[capacity]; } public void Add(double now, string line) { _ring[_next] = new Entry { T = now, Line = line }; _next = (_next + 1) % _ring.Length; } public IEnumerable<Entry> Ordered() { for (int i = 0; i < _ring.Length; i++) { Entry entry = _ring[(_next + i) % _ring.Length]; if (entry.Line != null) { yield return entry; } } } } public struct ViewFacts { public int Views; public int NonZeroIds; public int AlreadyAwake; public int Deregistered; } public struct SurvivorFacts { public int Views; public int NetControls; public int Disarmed; public int Queued; public bool PinnedByCharacter; } public static class ViewHygiene { public const string RefusalNote = "DestroyImmediate was REFUSED, not attempted-and-failed: Unity silently refuses it inside animation events, physics trigger/contact callbacks, StateMachineBehaviour callbacks and render callbacks — it logs an error, returns, and never throws, so no try/catch can see it."; public static string Describe(in ViewFacts f) { return $"views={f.Views} ids={f.NonZeroIds} awake={f.AlreadyAwake} deregistered={f.Deregistered}"; } public static string DescribeSurvivors(in SurvivorFacts f) { return $"pv={f.Views} ncc={f.NetControls} disarmed={f.Disarmed} queued={f.Queued}"; } public static bool IsClean(in SurvivorFacts f) { if (f.Views == 0) { return f.NetControls == 0; } return false; } } public enum LeaseVerdict { Keep, ReleaseNow, ReleaseAgedOut, NotePersist, WarnLeak } public struct MuteResult { public bool Muted; public int ViewId; public string Error; } public static class LeasePolicy { public const float MaxParkSeconds = 300f; public const int MintWarnWatermark = 800; public static bool ShouldWarnMintCount(int outstanding) { return outstanding >= 800; } public static LeaseVerdict Verdict(bool bodyGone, float ageSeconds, bool viewLostId, bool warned, bool mayPersist) { if (bodyGone) { return LeaseVerdict.ReleaseNow; } if (ageSeconds < 300f) { return LeaseVerdict.Keep; } if (viewLostId) { return LeaseVerdict.ReleaseAgedOut; } if (warned) { return LeaseVerdict.Keep; } if (!mayPersist) { return LeaseVerdict.WarnLeak; } return LeaseVerdict.NotePersist; } } public sealed class LeaseEntry<TBody> where TBody : class { public int ViewId; public TBody Body; public float QueuedAt; public bool Warned; public bool MayPersist; } public sealed class ViewLedger<TBody> where TBody : class { private readonly List<LeaseEntry<TBody>> _entries = new List<LeaseEntry<TBody>>(); public int Count => _entries.Count; public IReadOnlyList<LeaseEntry<TBody>> Entries => _entries; public LeaseEntry<TBody> Park(int viewId, TBody body, float now, bool mayPersist) { LeaseEntry<TBody> leaseEntry = new LeaseEntry<TBody> { ViewId = viewId, Body = body, QueuedAt = now, MayPersist = mayPersist }; _entries.Add(leaseEntry); return leaseEntry; } public bool Remove(LeaseEntry<TBody> entry) { return _entries.Remove(entry); } public int DropAll() { int count = _entries.Count; _entries.Clear(); return count; } public string DescribePending(string tag, float now, Func<TBody, string> bodyName) { if (_entries.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder($"{tag} {_entries.Count} viewID release(s) parked:"); foreach (LeaseEntry<TBody> entry in _entries) { string text = bodyName?.Invoke(entry.Body); stringBuilder.Append(string.Format("\n viewID={0} body='{1}' ", entry.ViewId, text ?? "<destroyed>") + $"age={now - entry.QueuedAt:0}s" + (entry.Warned ? (entry.MayPersist ? " PARKED(vanilla corpse — reserved until scene unload)" : " AGED-OUT(leak-warned)") : "")); } return stringBuilder.ToString(); } } public sealed class WarnOnceSet { private readonly HashSet<string> _warned = new HashSet<string>(StringComparer.Ordinal); public int Count => _warned.Count; public static string Key(int actor, string channelId) { return actor + "|" + channelId; } public bool ShouldWarn(int actor, string channelId) { return _warned.Add(Key(actor, channelId)); } public void Rearm(int actor, string channelId) { _warned.Remove(Key(actor, channelId)); } public void Clear() { _warned.Clear(); } } }
plugins/NetKit.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit.Core; using Photon; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("NetKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.10.0")] [assembly: AssemblyInformationalVersion("0.2.10+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("NetKit")] [assembly: AssemblyTitle("NetKit")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace NetKit { internal static class Diagnostics { private static readonly PunSignatureCounters _signatures = new PunSignatureCounters(); private static readonly UnknownViewTable _unknownViews = new UnknownViewTable(64, 500); private static bool _logHooked; private static float _nextHeartbeat; internal static readonly NoHandlerDrops NoHandler = new NoHandlerDrops(); internal static int UnknownViewWarnCount => _signatures.UnknownViewCount; internal static void Init() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (!_logHooked) { _logHooked = true; Application.logMessageReceived += new LogCallback(OnUnityLog); } } private static void OnUnityLog(string condition, string stackTrace, LogType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 if ((int)type != 2 && (int)type != 0 && (int)type != 1) { return; } int num = PunLog.Match(condition); if (num < 0) { return; } float unscaledTime = Time.unscaledTime; _signatures.Record(num, (double)unscaledTime); int num2 = default(int); if (num == 0 && PunLog.TryParseViewId(condition, ref num2)) { bool flag = default(bool); if (_unknownViews.Record(num2, (double)unscaledTime, ref flag)) { Plugin.Log.LogMessage((object)($"[NETKIT] unknown-view NEW id={num2} — something is streaming at a view this " + "machine doesn't hold (netdump shows the per-id table).")); } else if (flag) { Plugin.Log.LogWarning((object)($"[NETKIT] unknown-view RUNAWAY id={num2}: {_unknownViews.CountOf(num2)} warns and " + "climbing — a peer is continuously streaming at a view this machine doesn't hold (orphaned/evicted view; low ids 1-999 are scene-baked — donor-harvest eviction class). netdump shows the per-id table; a relaunch of the streaming peer clears it.")); } } } internal static void HeartbeatTick() { if (!PhotonNetwork.inRoom) { return; } float num = ((Plugin.HeartbeatSeconds != null) ? Plugin.HeartbeatSeconds.Value : 0f); if (num <= 0f || Time.unscaledTime < _nextHeartbeat) { return; } _nextHeartbeat = Time.unscaledTime + num; int num2 = 0; try { num2 = ((PhotonNetwork.otherPlayers != null) ? PhotonNetwork.otherPlayers.Length : 0); } catch { } double time = PhotonNetwork.time; bool isMasterClient = PhotonNetwork.isMasterClient; foreach (NetChannel channel in Net.Channels) { if (!(channel.Id == "nk")) { Plugin.Log.LogMessage((object)Heartbeat.Line(channel.LogTag, isMasterClient, num2, channel.ReadyCount, channel.Counters.TotalSends, channel.Counters.TotalRecvs, channel.Counters.TotalDrops, channel.HeartbeatFragmentValue(), time)); } } if (UnknownViewWarnCount > 0) { string text = _unknownViews.TopSummary(4); Plugin.Log.LogMessage((object)($"[NETKIT] hb unknownViewWarns={UnknownViewWarnCount} pt={time:F1}" + ((text.Length > 0) ? (" top=[" + text + "]") : ""))); } } internal static string Dump() { //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_02d0: Unknown result type (might be due to invalid IL or missing references) //IL_02d5: Unknown result type (might be due to invalid IL or missing references) //IL_02de: Unknown result type (might be due to invalid IL or missing references) //IL_02e0: Unknown result type (might be due to invalid IL or missing references) //IL_0342: Unknown result type (might be due to invalid IL or missing references) //IL_0344: Unknown result type (might be due to invalid IL or missing references) //IL_02ec: Unknown result type (might be due to invalid IL or missing references) //IL_02ee: Unknown result type (might be due to invalid IL or missing references) //IL_0355: Unknown result type (might be due to invalid IL or missing references) //IL_0357: Unknown result type (might be due to invalid IL or missing references) //IL_036f: Unknown result type (might be due to invalid IL or missing references) //IL_0371: Unknown result type (might be due to invalid IL or missing references) //IL_0303: Unknown result type (might be due to invalid IL or missing references) //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_030c: Unknown result type (might be due to invalid IL or missing references) //IL_0318: Unknown result type (might be due to invalid IL or missing references) //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03aa: Unknown result type (might be due to invalid IL or missing references) //IL_03ac: Unknown result type (might be due to invalid IL or missing references) //IL_03c3: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); INetTransport transport = Net.Transport; stringBuilder.AppendLine($"[NETKIT] transport={transport.Name} attached={transport.Attached} inRoom={PhotonNetwork.inRoom} " + $"isMaster={PhotonNetwork.isMasterClient} isGuest={PhotonNetwork.isNonMasterClientInRoom} " + $"pt={PhotonNetwork.time:F1} seqTx={Net.NextSeqPreview} helloMuted={HelloService.HelloMuted}"); stringBuilder.AppendLine("[NETKIT] " + transport.AttachInfo()); stringBuilder.AppendLine("[NETKIT] local build " + Net.LocalBuildStamp); stringBuilder.AppendLine(LoaderLine()); stringBuilder.AppendLine($"[NETKIT] {Net.ChannelCount} channel(s) registered."); foreach (NetChannel channel in Net.Channels) { List<string> list = new List<string>(channel.Verbs); list.Sort(StringComparer.Ordinal); stringBuilder.AppendLine(string.Format("[{0}] channel '{1}' v{2} — {3} verb(s): {4}", channel.LogTag, channel.Id, channel.Version, list.Count, string.Join(", ", list.ToArray()))); stringBuilder.AppendLine($"[{channel.LogTag}] peers ready: {channel.ReadyCount}"); stringBuilder.AppendLine(channel.Counters.Summary(channel.LogTag, (double)Time.unscaledTime)); bool flag = false; foreach (Entry item in channel.Trace.Ordered()) { if (!flag) { stringBuilder.AppendLine("[" + channel.LogTag + "] last events (newest last):"); flag = true; } stringBuilder.AppendLine($"[{channel.LogTag}] {item.T:F0}s {item.Line}"); } if (channel.Stores == null) { continue; } foreach (ReplicatedStore store in channel.Stores) { stringBuilder.AppendLine(store.Dump()); } } stringBuilder.AppendLine(NoHandler.Summary("NETKIT")); if (HelloService.Peers.Count == 0) { stringBuilder.AppendLine("[NETKIT] no peer hellos received."); } foreach (Entry entry in HelloService.Peers.Entries) { List<string> list2 = new List<string>(); if (entry.Hello.Channels != null) { foreach (ChannelHello channel2 in entry.Hello.Channels) { list2.Add(channel2.Channel + "=" + channel2.Version); } } string text = (string.IsNullOrEmpty(entry.Hello.Stamp) ? "(none)" : entry.Hello.Stamp); string text2 = (BuildStampCompat.ShouldWarn(Net.LocalBuildStamp, entry.Hello.Stamp) ? " ⚠DIFFERS" : ""); stringBuilder.AppendLine($"[NETKIT] peer actor {entry.Actor}: proto v{entry.Hello.Proto} " + ((PhotonPlayer.Find(entry.Actor) != null) ? "(in room)" : "(GONE)") + " build " + text + text2 + " — " + string.Join(", ", list2.ToArray())); } foreach (KeyValuePair<int, double> item2 in HelloService.Ledger.Pending) { stringBuilder.AppendLine($"[NETKIT] awaiting hello from actor {item2.Key} " + $"({HelloService.Ledger.GraceElapsed(item2.Key, (double)Time.unscaledTime):F0}s grace elapsed)."); } string value = _signatures.Summary("NETKIT"); if (!string.IsNullOrEmpty(value)) { stringBuilder.AppendLine(value); } if (_unknownViews.IdCount > 0) { stringBuilder.AppendLine(_unknownViews.Format("NETKIT")); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } private static string LoaderLine() { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { return "[NETKIT] loader: n/a (NetworkLevelLoader.Instance null — main menu?)"; } try { return $"[NETKIT] loader: gameplayPaused={instance.IsGameplayPaused} " + $"allPlayersDone={instance.AllPlayerDoneLoading} overallDone={instance.IsOverallLoadingDone} " + $"pausedBy=[{PausedByKeys(instance)}] otherPaused={instance.m_otherPlayerPaused} " + $"msgQueue={PhotonNetwork.isMessageQueueRunning} timeScale={Time.timeScale:F2}"; } catch (Exception ex) { return "[NETKIT] loader: n/a (read threw: " + ex.GetType().Name + ")"; } } private static string PausedByKeys(NetworkLevelLoader nll) { try { List<string> list = ((nll.m_gameplayPausedBy != null) ? nll.m_gameplayPausedBy.Keys : null); if (list == null || list.Count == 0) { return ""; } return string.Join(",", list.ToArray()); } catch (Exception ex) { return "threw:" + ex.GetType().Name; } } } internal sealed class EventTransport : INetTransport { private readonly byte _code; private bool _subscribed; public string Name => "Event"; public bool Attached => _subscribed; public EventTransport(int eventCode) { if (eventCode < 0 || eventCode > 199) { Plugin.Log.LogWarning((object)($"[NETKIT] Event transport code {eventCode} is outside Photon's 0..199 " + "custom-event range — clamped to 177. Fix [Net] EventCode so both boxes agree.")); eventCode = 177; } _code = (byte)eventCode; } public bool TryAttach() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown if (_subscribed) { return true; } try { PhotonNetwork.OnEventCall += new EventCallback(OnEvent); _subscribed = true; Plugin.Log.LogMessage((object)$"[NETKIT] Event transport subscribed (code {_code})."); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[NETKIT] Event subscribe threw: " + ex.Message)); return false; } } public bool Send(SendTarget target, PhotonPlayer specific, string channel, string verb, int seq, string extra, string payload) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if (!TryAttach()) { return false; } try { object[] array = new object[5] { channel, verb, seq, extra, payload }; RaiseEventOptions val = new RaiseEventOptions(); if (target == SendTarget.Player) { if (specific == null) { return false; } val.TargetActors = new int[1] { specific.ID }; } else { val.Receivers = ToReceiverGroup(target); } return PhotonNetwork.RaiseEvent(_code, (object)array, true, val); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"[NETKIT] Event send '{channel}.{verb}' seq={seq} failed: {ex.Message}"); return false; } } public bool SendLoopback(string channel, string verb, int seq, string extra, string payload) { try { int senderId = ((PhotonNetwork.player != null) ? PhotonNetwork.player.ID : (-1)); Net.DeliverEvent(channel, verb, seq, extra, payload, senderId); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[NETKIT] Event loopback '" + channel + "." + verb + "' failed: " + ex.Message)); return false; } } public string AttachInfo() { return $"event-code={_code} subscribed={_subscribed}"; } private void OnEvent(byte eventCode, object content, int senderId) { if (eventCode != _code || !(content is object[] array) || array.Length < 5) { return; } try { string channel = array[0] as string; string verb = array[1] as string; int seq = ((array[2] is int num) ? num : 0); string extra = (array[3] as string) ?? ""; string payload = (array[4] as string) ?? ""; Net.DeliverEvent(channel, verb, seq, extra, payload, senderId); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[NETKIT] Event receive threw: " + ex.Message)); } } private static ReceiverGroup ToReceiverGroup(SendTarget t) { return (ReceiverGroup)(t switch { SendTarget.Master => 2, SendTarget.All => 1, _ => 0, }); } } internal static class HelloService { private static readonly PeerHelloStore _peers = new PeerHelloStore(); private static readonly PeerLedger _ledger = new PeerLedger(); private static readonly HashSet<int> _buildWarned = new HashSet<int>(); private static readonly WarnOnceSet _incompatWarned = new WarnOnceSet(); private static readonly HashSet<int> _malformedWarned = new HashSet<int>(); private static readonly RoomWatch _roomWatch = new RoomWatch(); private static readonly SceneReadyGate _sceneGate = new SceneReadyGate(); private static HelloResend _resend = new HelloResend(5.0); private static float _warnSeconds = 10f; internal static bool HelloMuted; internal static PeerHelloStore Peers => _peers; internal static PeerLedger Ledger => _ledger; internal static void Configure(float warnSeconds, float refreshSeconds) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown _warnSeconds = ((warnSeconds > 0f) ? warnSeconds : 10f); _resend = new HelloResend((double)refreshSeconds); } internal static bool LocalSceneReady() { Character val = default(Character); if ((Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsOverallLoadingDone) { return Lifecycle.TryGetFirstLocalCharacter(ref val); } return false; } internal static void OnHello(NetMessage msg) { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) _ledger.MarkHello(msg.SenderActor); HelloMsg val = default(HelloMsg); if (!HelloCodec.TryDecode(msg.Payload, ref val)) { Net.TryGetChannel("nk", out var ch); ch?.CountDrop("nk.hello", "unparseable"); if (_malformedWarned.Add(msg.SenderActor)) { Plugin.Log.LogWarning((object)($"[NETKIT] malformed hello from actor {msg.SenderActor}: '{msg.Payload}'. " + "The peer IS modded but its hello is unreadable — every co-op feature will REFUSE toward it (no channel becomes ready, and the appears-UNMODDED warning stands down because the peer provably messages). Mismatched builds are the usual cause. (warned once per peer per room; further malformed hellos counted silently)")); } } else { Entry val2 = default(Entry); bool hadPrev = _peers.TryGet(msg.SenderActor, ref val2); _peers.Record(msg.SenderActor, val, msg.SenderIsMaster); WarnOnBuildMismatch(msg.SenderActor, val.Stamp); ApplyReadiness(msg.SenderActor, val2.Hello, hadPrev, val, msg.SenderIsMaster); } } private static void WarnOnBuildMismatch(int actor, string theirStamp) { if (!_buildWarned.Contains(actor)) { string localBuildStamp = Net.LocalBuildStamp; if (BuildStampCompat.ShouldWarn(localBuildStamp, theirStamp)) { _buildWarned.Add(actor); Plugin.Log.LogWarning((object)($"[NETKIT] peer actor {actor} build differs — ours {localBuildStamp}, " + "theirs " + theirStamp + " (stale install?)")); } } } private static void ApplyReadiness(int actor, HelloMsg prev, bool hadPrev, HelloMsg hello, bool isMaster) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) List<string> newlyIncompatible = new List<string>(); EventFanout.ForEach<NetChannel>((IEnumerable<NetChannel>)Net.Channels, (Action<NetChannel>)delegate(NetChannel ch) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) if (!(ch.Id == "nk")) { string text = HelloCodec.VersionOf(ref hello, ch.Id); string text2 = HelloCodec.ExtensionOf(ref hello, ch.Id) ?? ""; Kind val = ChannelCompat.Evaluate(1, hello.Proto, ch.Version, text); PeerInfo info = new PeerInfo { Actor = actor, ChannelVersion = (text ?? ""), Extension = text2, IsMaster = isMaster }; if (ChannelCompat.IsReady(val)) { bool flag = ch.SetSceneReady(actor, hello.SceneReady); if (ch.IsPeerReady(actor)) { string text3 = (hadPrev ? (HelloCodec.ExtensionOf(ref prev, ch.Id) ?? "") : ""); if (ExtensionChange.ShouldRaiseChanged(true, hadPrev, text3, text2)) { ch.RaiseExtensionChanged(info); } } else { ch.MarkReady(actor, info); } if (flag) { ch.RaiseSceneReady(info); } _incompatWarned.Rearm(actor, ch.Id); } else { ch.MarkLost(actor, info); if (_incompatWarned.ShouldWarn(actor, ch.Id)) { newlyIncompatible.Add(ch.Id); } } } }, (Action<NetChannel, Exception>)delegate(NetChannel ch, Exception e) { Plugin.Log.LogWarning((object)("[NETKIT] readiness evaluation for channel '" + ch.Id + "' threw " + $"(hello from actor {actor}) — later channels still evaluated: {e}")); }); if (newlyIncompatible.Count > 0) { Plugin.Log.LogWarning((object)($"[NETKIT] hello from actor {actor} (proto v{hello.Proto}) is INCOMPATIBLE on channel(s) " + string.Join(", ", newlyIncompatible.ToArray()) + " — those features stay inert toward this peer (netdump shows per-channel readiness; warned once per peer/channel).")); } } internal static void ReplayForChannel(NetChannel ch) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0140: 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_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) if (ch == null || ch.Id == "nk" || _peers.Count == 0) { return; } Entry val = default(Entry); foreach (ChannelReadiness item in _peers.EvaluateChannel(1, ch.Id, ch.Version)) { PeerInfo info = new PeerInfo { Actor = item.Actor, ChannelVersion = item.Version, Extension = item.Extension, IsMaster = item.IsMaster }; if (item.Ready) { Plugin.Log.LogMessage((object)("[NETKIT] late-registered channel '" + ch.Id + "' — replayed stored hello from " + $"actor {item.Actor}: ready (v{item.Version}).")); bool flag = _peers.TryGet(item.Actor, ref val) && ch.SetSceneReady(item.Actor, val.Hello.SceneReady); ch.MarkReady(item.Actor, info); if (flag) { ch.RaiseSceneReady(info); } } else { Plugin.Log.LogWarning((object)("[NETKIT] late-registered channel '" + ch.Id + "' — replayed stored hello from " + $"actor {item.Actor}: {item.Kind} (feature stays inert toward this peer).")); } } } internal static void RecordHelloSent(string payload, int? toActor) { if (toActor.HasValue) { _resend.RecordSentTo(toActor.Value, payload); } else { _resend.RecordSent(payload); } } internal static void Tick() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) bool inRoom = PhotonNetwork.inRoom; string text = null; if (inRoom && PhotonNetwork.room != null) { text = PhotonNetwork.room.Name; } RoomChange val = default(RoomChange); if (_roomWatch.Update(inRoom, text, ref val)) { _sceneGate.Reset(); Plugin.Log.LogMessage((object)("[NETKIT] room changed: '" + (val.OldRoomName ?? "(none)") + "' -> '" + (val.NewRoomName ?? "(none)") + "'.")); Net.RaiseRoomChanged(val); } if (!inRoom) { int num = 0; foreach (NetChannel channel in Net.Channels) { num += channel.ReadyCount; } if (OutOfRoomReset.ShouldReset(_ledger.PendingCount, _ledger.HelloedCount, _peers.Count, num)) { _ledger.Reset(); _peers.Clear(); _buildWarned.Clear(); _incompatWarned.Clear(); _malformedWarned.Clear(); foreach (NetChannel channel2 in Net.Channels) { channel2.ResetPeers(); } } _resend.Reset(); return; } List<int> list = _ledger.DueForWarning((double)Time.unscaledTime, (double)_warnSeconds); if (list != null) { foreach (int item in list) { if (PhotonPlayer.Find(item) != null) { Plugin.Log.LogWarning((object)(Net.Attached ? ($"[NETKIT] no hello from actor {item} {_warnSeconds:F0}s after joining — peer appears UNMODDED " + "(no NetKit): every co-op feature is inert toward them.") : ($"[NETKIT] no hello from actor {item} {_warnSeconds:F0}s after joining — but the LOCAL transport " + "never attached (see the [NETKIT] attach lines), so whether the peer is modded is unknowable."))); } } } bool flag = LocalSceneReady(); object obj; if (!flag) { obj = null; } else { Scene activeScene = SceneManager.GetActiveScene(); obj = ((Scene)(ref activeScene)).name; } string text2 = (string)obj; if (_sceneGate.Fire(true, flag, text2)) { Net.RaiseSceneReady(text2); if (!PhotonNetwork.isMasterClient) { Plugin.Log.LogMessage((object)("[NETKIT] scene-ready in '" + text2 + "' — re-hello (peer-ready resync).")); Net.SendHello(null); } } if (_resend.DueForCheck((double)Time.unscaledTime) && _resend.PayloadChanged(Net.BuildHello())) { Plugin.Log.LogMessage((object)"[NETKIT] hello payload changed since last send — re-hello (extension refresh; peers re-diff without a scene change)."); Net.SendHello(null); } } internal static void OnPeerConnected(PhotonPlayer newPlayer) { if (newPlayer != null && PhotonNetwork.inRoom) { Net.SendHello(newPlayer); _ledger.Expect(newPlayer.ID, (double)Time.unscaledTime); } } internal static void OnSelfJoinedRoom() { if (!PhotonNetwork.inRoom || PhotonNetwork.room == null) { return; } ResetLedger(); if (PhotonNetwork.room.PlayerCount <= 1) { return; } Net.SendHello(null); PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; foreach (PhotonPlayer val in otherPlayers) { if (val != null) { _ledger.Expect(val.ID, (double)Time.unscaledTime); } } } internal static void OnPeerDisconnected(PhotonPlayer player) { if (player == null) { return; } int actor = player.ID; _resend.ForgetActor(actor); Entry entry = default(Entry); _peers.TryGet(actor, ref entry); _peers.Remove(actor); EventFanout.ForEach<NetChannel>((IEnumerable<NetChannel>)Net.Channels, (Action<NetChannel>)delegate(NetChannel ch) { if (!(ch.Id == "nk")) { ch.MarkLost(actor, new PeerInfo { Actor = actor, ChannelVersion = (HelloCodec.VersionOf(ref entry.Hello, ch.Id) ?? ""), Extension = (HelloCodec.ExtensionOf(ref entry.Hello, ch.Id) ?? ""), IsMaster = entry.IsMaster }); } }, (Action<NetChannel, Exception>)delegate(NetChannel ch, Exception e) { Plugin.Log.LogWarning((object)("[NETKIT] peer-lost teardown for channel '" + ch.Id + "' threw " + $"(actor {actor}) — later channels still evaluated: {e}")); }); } private static void ResetLedger() { if (_ledger.PendingCount > 0 || _ledger.HelloedCount > 0) { Plugin.Log.LogMessage((object)($"[NETKIT] peer ledger reset on room join ({_ledger.HelloedCount} hello(s), " + $"{_ledger.PendingCount} pending — actor IDs restart per room).")); } _ledger.Reset(); _peers.Clear(); _buildWarned.Clear(); _incompatWarned.Clear(); _malformedWarned.Clear(); _resend.Reset(); foreach (NetChannel channel in Net.Channels) { channel.ResetPeers(); } } } internal enum SendTarget { Others, Master, All, Player } internal interface INetTransport { string Name { get; } bool Attached { get; } bool TryAttach(); bool Send(SendTarget target, PhotonPlayer specific, string channel, string verb, int seq, string extra, string payload); bool SendLoopback(string channel, string verb, int seq, string extra, string payload); string AttachInfo(); } [HarmonyPatch(typeof(NetworkLevelLoader), "OnPhotonPlayerConnected")] internal static class NetKitPeerJoinedHook { [HarmonyPostfix] private static void Postfix(PhotonPlayer _newPlayer) { try { HelloService.OnPeerConnected(_newPlayer); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[NETKIT] peer-connected hook threw: {arg}"); } } } [HarmonyPatch(typeof(NetworkLevelLoader), "OnJoinedRoom")] internal static class NetKitSelfJoinedHook { [HarmonyPostfix] private static void Postfix() { try { HelloService.OnSelfJoinedRoom(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[NETKIT] self-joined hook threw: {arg}"); } } } [HarmonyPatch(typeof(NetworkLevelLoader), "OnPhotonPlayerDisconnected")] internal static class NetKitPeerLeftHook { [HarmonyPostfix] private static void Postfix(PhotonPlayer _player) { try { HelloService.OnPeerDisconnected(_player); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[NETKIT] peer-disconnected hook threw: {arg}"); } } } public enum NetTransportKind { Rpc, Event, Sim } public static class Net { public const string RpcName = "NK_Bus"; public const string InternalChannelId = "nk"; public const string HelloVerb = "nk.hello"; public const string TestVerb = "nk.test"; public const int ProtoVersion = 1; public static readonly string LocalBuildStamp = BuildStamp.Read(typeof(Net).Assembly); private static readonly Dictionary<string, NetChannel> _channels = new Dictionary<string, NetChannel>(StringComparer.Ordinal); private static readonly List<NetChannel> _ordered = new List<NetChannel>(); private static INetTransport _transport; private static NetChannel _internal; private static int _seqTx; private static bool _unavailableLogged; private static bool _inited; internal static string LastTestPayload; private const int UnknownChannelWarnCap = 32; private static readonly HashSet<string> _unknownChannelWarned = new HashSet<string>(StringComparer.Ordinal); private static readonly HashSet<int> _loopbackPeers = new HashSet<int>(); private static readonly PendingOnce<NetChannel> _pendingReplays = new PendingOnce<NetChannel>(); public static bool Attached { get { if (_transport != null) { return _transport.Attached; } return false; } } public static bool InRoom => PhotonNetwork.inRoom; public static bool IsMaster => PhotonNetwork.isMasterClient; public static bool IsGuestInRoom => PhotonNetwork.isNonMasterClientInRoom; internal static INetTransport Transport => _transport; internal static IReadOnlyList<NetChannel> Channels => _ordered; internal static NetChannel Internal => _internal; public static int ChannelCount => _ordered.Count; internal static int NextSeqPreview => _seqTx; public static event Action<RoomChange> OnRoomChanged; public static event Action<string> OnSceneReady; internal static void RaiseRoomChanged(RoomChange change) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) foreach (NetChannel item in _ordered) { try { item.AbortRequestsOnRoomChange(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[NETKIT] room-change request drain for channel '{item.Id}' threw: {arg}"); } } Action<RoomChange> onRoomChanged = Net.OnRoomChanged; if (onRoomChanged == null) { return; } Delegate[] invocationList = onRoomChanged.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action<RoomChange>)obj)(change); } catch (Exception arg2) { Plugin.Log.LogWarning((object)("[NETKIT] an OnRoomChanged subscriber threw " + $"({obj.Method.DeclaringType?.FullName}.{obj.Method.Name}): {arg2}")); } } } internal static void RaiseSceneReady(string scene) { Action<string> onSceneReady = Net.OnSceneReady; if (onSceneReady == null) { return; } Delegate[] invocationList = onSceneReady.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action<string>)obj)(scene); } catch (Exception arg) { Plugin.Log.LogWarning((object)("[NETKIT] an OnSceneReady subscriber threw " + $"({obj.Method.DeclaringType?.FullName}.{obj.Method.Name}): {arg}")); } } } internal static void Init(NetTransportKind kind, int eventCode) { if (!_inited) { _inited = true; INetTransport transport; switch (kind) { default: { INetTransport netTransport = new RpcRelayTransport(); transport = netTransport; break; } case NetTransportKind.Sim: { INetTransport netTransport = new SimTransport(); transport = netTransport; break; } case NetTransportKind.Event: { INetTransport netTransport = new EventTransport(eventCode); transport = netTransport; break; } } _transport = transport; _internal = RegisterChannel("nk", "0.2.10", new ChannelOptions { LogTag = "NETKIT" }); _internal.Register("nk.hello", HelloService.OnHello); _internal.Register("nk.test", delegate(NetMessage m) { LastTestPayload = m.Payload; }); _transport.TryAttach(); Plugin.Log.LogMessage((object)($"[NETKIT] transport={_transport.Name} attached={_transport.Attached} — " + _transport.AttachInfo())); } } public static NetChannel RegisterChannel(string id, string version, ChannelOptions options = null) { if (string.IsNullOrEmpty(id)) { throw new ArgumentException("channel id required", "id"); } if (_channels.TryGetValue(id, out var value)) { if (options != null) { Plugin.Log.LogWarning((object)("[NETKIT] RegisterChannel('" + id + "') called again WITH options — the id is already registered (v" + value.Version + ", tag [" + value.LogTag + "]) and the new options are DISCARDED. Two consumers claiming one channel id share the first caller's configuration.")); } return value; } NetChannel netChannel = new NetChannel(id, version, options); Plugin.Log.LogMessage((object)("[NETKIT] channel '" + id + "' v" + netChannel.Version + " registered (tag [" + netChannel.LogTag + "]).")); _channels[id] = netChannel; _ordered.Add(netChannel); _pendingReplays.Add(netChannel); return netChannel; } internal static bool TryGetChannel(string id, out NetChannel ch) { return _channels.TryGetValue(id ?? "", out ch); } internal static bool Send(NetChannel ch, SendTarget target, PhotonPlayer specific, string verb, string extra, string payload) { if (ch == null) { return false; } if (!PhotonNetwork.inRoom) { ch.CountDrop(verb, "not-in-room"); return false; } if (_transport == null) { ch.CountDrop(verb, "not-inited"); return false; } if (!_transport.TryAttach()) { ch.CountDrop(verb, "not-attached"); if (!_unavailableLogged) { _unavailableLogged = true; Plugin.Log.LogWarning((object)("[NETKIT] send dropped — transport '" + _transport.Name + "' never attached (" + _transport.AttachInfo() + "). Co-op messages are disabled until it does (retried every send).")); } return false; } _unavailableLogged = false; int num = ++_seqTx; bool flag = _transport.Send(target, specific, ch.Id, verb, num, extra ?? "", payload ?? ""); float unscaledTime = Time.unscaledTime; if (flag) { ch.Counters.RecordSend(verb, (double)unscaledTime); if (!ch.Quiet.IsQuiet(verb)) { ch.Trace.Add((double)unscaledTime, $"{Dir()} send {verb} seq={num} {TargetTag(target, specific)}"); if (Plugin.VerboseNet == null || Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)$"[{ch.LogTag}] {Dir()} send {verb} seq={num} {TargetTag(target, specific)}"); } } } else { ch.CountDrop(verb, "send-failed"); } return flag; } internal static bool SendLoopback(NetChannel ch, string verb, string extra, string payload) { if (ch == null) { return false; } if (_transport == null) { ch.CountDrop(verb, "not-inited"); return false; } if (!_transport.TryAttach()) { ch.CountDrop(verb, "not-attached"); return false; } _unavailableLogged = false; int seq = ++_seqTx; bool flag = _transport.SendLoopback(ch.Id, verb, seq, extra ?? "", payload ?? ""); if (flag) { ch.Counters.RecordSend(verb, (double)Time.unscaledTime); } else { ch.CountDrop(verb, "loopback-failed"); } return flag; } internal static void DeliverRpc(string channel, string verb, int seq, string extra, string payload, PhotonMessageInfo info) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) int actor = ((info.sender != null) ? info.sender.ID : 0); bool isMaster = info.sender != null && info.sender.IsMasterClient; bool isSelf = info.sender != null && PhotonNetwork.player != null && info.sender.ID == PhotonNetwork.player.ID; Deliver(channel, verb, seq, extra, payload, actor, isMaster, isSelf); } internal static void DeliverEvent(string channel, string verb, int seq, string extra, string payload, int senderId) { PhotonPlayer val = PhotonPlayer.Find(senderId); bool isMaster = val != null && val.IsMasterClient; bool isSelf = PhotonNetwork.player != null && senderId == PhotonNetwork.player.ID; Deliver(channel, verb, seq, extra, payload, senderId, isMaster, isSelf); } private static void Deliver(string channel, string verb, int seq, string extra, string payload, int actor, bool isMaster, bool isSelf) { if (!_channels.TryGetValue(channel ?? "", out var value)) { if (_unknownChannelWarned.Count < 32 && _unknownChannelWarned.Add(channel ?? "?")) { Plugin.Log.LogWarning((object)($"[NETKIT] traffic for unregistered channel '{channel}' (actor {actor}) " + "— a mod that isn't installed here, or a newer build. Ignored. (warned once per channel" + $", capped at {32} distinct names)")); } return; } float unscaledTime = Time.unscaledTime; value.Counters.RecordRecv(verb, (double)unscaledTime); if (!value.Quiet.IsQuiet(verb)) { value.Trace.Add((double)unscaledTime, $"{RecvDir(isMaster)} recv {verb} seq={seq} actor={actor}"); if (Plugin.VerboseNet == null || Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)$"[{value.LogTag}] {RecvDir(isMaster)} recv {verb} seq={seq} actor={actor}"); } } value.Deliver(new NetMessage { Verb = verb, Payload = payload, Extra = extra, SenderActor = actor, SenderIsMaster = isMaster, SenderIsSelf = isSelf }); } internal static string BuildHello() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) HelloMsg val = new HelloMsg { Channels = new List<ChannelHello>() }; val.Proto = 1; val.Stamp = LocalBuildStamp; val.SceneReady = HelloService.LocalSceneReady(); foreach (NetChannel item in _ordered) { if (!(item.Id == "nk")) { val.Channels.Add(new ChannelHello { Channel = item.Id, Version = item.Version, Extension = item.HelloExtensionValue() }); } } return HelloCodec.Encode(val); } public static void RegisterLoopbackPeer(int actor) { if (_loopbackPeers.Add(actor)) { DeliverEvent("nk", "nk.hello", 0, "", BuildHello(), actor); Plugin.Log.LogMessage((object)($"[NETKIT] loopback peer registered: actor {actor} now answers " + "hellos as a same-bundle install (dev rig).")); } } public static void UnregisterLoopbackPeer(int actor) { if (_loopbackPeers.Remove(actor)) { Plugin.Log.LogMessage((object)$"[NETKIT] loopback peer unregistered: actor {actor}."); } } internal static bool SendHello(PhotonPlayer to) { if (HelloService.HelloMuted) { _internal?.CountDrop("nk.hello", "muted"); return false; } string payload = BuildHello(); bool flag = ((to != null) ? _internal.SendToPlayer(to, "nk.hello", payload) : _internal.SendToOthers("nk.hello", payload)); if (flag) { HelloService.RecordHelloSent(payload, (to != null) ? new int?(to.ID) : ((int?)null)); } if (flag && _loopbackPeers.Count > 0) { foreach (int loopbackPeer in _loopbackPeers) { if (to == null || to.ID == loopbackPeer) { DeliverEvent("nk", "nk.hello", 0, "", BuildHello(), loopbackPeer); } } } return flag; } public static string Dump() { return Diagnostics.Dump(); } internal static void Tick() { List<NetChannel> list = _pendingReplays.Drain(); if (list != null) { foreach (NetChannel item in list) { HelloService.ReplayForChannel(item); } } HelloService.Tick(); Diagnostics.HeartbeatTick(); float unscaledTime = Time.unscaledTime; for (int i = 0; i < _ordered.Count; i++) { _ordered[i].RequestTick(unscaledTime); _ordered[i].StoreTick(unscaledTime); } } private static string Dir() { if (!PhotonNetwork.isMasterClient) { return "[G→M]"; } return "[M→G]"; } private static string RecvDir(bool senderIsMaster) { if (!senderIsMaster) { return "[G→M]"; } return "[M→G]"; } private static string TargetTag(SendTarget t, PhotonPlayer specific) { if (t != SendTarget.Player || specific == null) { return $"to={t}"; } return $"to=actor{specific.ID}"; } } public struct NetMessage { public string Verb; public string Payload; public string Extra; public int SenderActor; public bool SenderIsMaster; public bool SenderIsSelf; } public struct PeerInfo { public int Actor; public string ChannelVersion; public string Extension; public bool IsMaster; } public sealed class ChannelOptions { public string LogTag; public Func<string> HelloExtension; public Func<string> HeartbeatFragment; public string[] QuietVerbs; } public sealed class NetChannel { private struct VerbEntry { public Action<NetMessage> Handler; public HandlerRole Role; } private readonly Dictionary<string, VerbEntry> _verbs = new Dictionary<string, VerbEntry>(StringComparer.Ordinal); internal readonly NetCounters Counters = new NetCounters(); internal readonly TraceRing Trace = new TraceRing(32); internal readonly QuietVerbSet Quiet; private readonly HashSet<int> _ready = new HashSet<int>(); private readonly HashSet<int> _sceneReady = new HashSet<int>(); private List<ReplicatedStore> _stores; private RequestHub _requests; internal const string ExtensionProviderThrew = "provider-threw"; internal const string HeartbeatProviderThrew = "hb-frag-threw"; private bool _extensionThrewLogged; private bool _heartbeatThrewLogged; public string Id { get; } public string Version { get; } public string LogTag { get; } internal ChannelOptions Options { get; } internal IReadOnlyList<ReplicatedStore> Stores => _stores; private RequestHub Requests => _requests ?? (_requests = new RequestHub(this)); public int ReadyCount => _ready.Count; internal IEnumerable<string> Verbs => _verbs.Keys; public event Action<PeerInfo> OnPeerReady; public event Action<PeerInfo> OnPeerSceneReady; public event Action<PeerInfo> OnPeerExtensionChanged; public event Action<PeerInfo> OnPeerLost; internal NetChannel(string id, string version, ChannelOptions options) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown //IL_00b6: Unknown result type (might be due to invalid IL or missing references) Id = id; Version = version ?? ""; Options = options ?? new ChannelOptions(); LogTag = ((!string.IsNullOrEmpty(Options.LogTag)) ? Options.LogTag : id.ToUpperInvariant()); Quiet = (QuietVerbSet)((Options.QuietVerbs == null) ? ((object)QuietVerbSet.Empty) : ((object)new QuietVerbSet((IEnumerable<string>)Options.QuietVerbs))); } public void Register(string verb, Action<NetMessage> handler) { Register(verb, handler, (HandlerRole)0); } public void Register(string verb, Action<NetMessage> handler, HandlerRole role) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(verb) && handler != null) { _verbs[verb] = new VerbEntry { Handler = handler, Role = role }; } } public bool SendToMaster(string verb, string payload, string extra = "") { return Net.Send(this, SendTarget.Master, null, verb, extra, payload); } public bool SendToOthers(string verb, string payload, string extra = "") { return Net.Send(this, SendTarget.Others, null, verb, extra, payload); } public bool SendToAll(string verb, string payload, string extra = "") { return Net.Send(this, SendTarget.All, null, verb, extra, payload); } public bool SendToPlayer(PhotonPlayer player, string verb, string payload, string extra = "") { if (player != null) { return Net.Send(this, SendTarget.Player, player, verb, extra, payload); } return false; } public bool SendToActor(int actorId, string verb, string payload, string extra = "") { PhotonPlayer val = PhotonPlayer.Find(actorId); if (val == null) { CountDrop(verb, "actor-gone"); return false; } return Net.Send(this, SendTarget.Player, val, verb, extra, payload); } public bool SendToAllLoopback(string verb, string payload, string extra = "") { return Net.SendLoopback(this, verb, extra, payload); } public void CountDrop(string verb, string reason) { Counters.RecordDrop(verb, reason, (double)Time.unscaledTime); } public StateMirror Mirror(string verb, MirrorTarget target, Func<string> build, Func<string, string> quantize = null) { return Mirror(verb, target, build, new MirrorOptions { Quantize = quantize }); } public StateMirror Mirror(string verb, MirrorTarget target, Func<string> build, MirrorOptions options) { if (string.IsNullOrEmpty(verb)) { throw new ArgumentException("mirror verb required", "verb"); } if (build == null) { throw new ArgumentNullException("build"); } return new StateMirror(this, verb, target, build, options); } public ReplicatedStore RegisterStore(string name, StoreOptions options) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("store name required", "name"); } if (_stores != null) { foreach (ReplicatedStore store in _stores) { if (string.Equals(store.Name, name, StringComparison.Ordinal)) { Plugin.Log.LogWarning((object)("[" + LogTag + "] RegisterStore('" + name + "') called again — the name is already registered on this channel and the new options are DISCARDED.")); return store; } } } ReplicatedStore replicatedStore = new ReplicatedStore(this, name, options); (_stores ?? (_stores = new List<ReplicatedStore>())).Add(replicatedStore); Plugin.Log.LogMessage((object)("[" + LogTag + "] store '" + name + "' registered " + $"(authority={options?.Authority ?? StoreAuthority.Master}, verbs " + "'" + options?.Verbs?.Announce + "'/'" + options?.Verbs?.Release + "').")); return replicatedStore; } internal void StoreTick(float now) { if (_stores != null) { for (int i = 0; i < _stores.Count; i++) { _stores[i].Tick(now); } } } public bool SendRequest(string verb, string payload, float timeoutSeconds, Action<RequestResult> onResult, string extra = "", bool singleFlight = true) { return Requests.SendRequest(verb, payload, timeoutSeconds, onResult, extra, singleFlight); } public void RegisterRequestHandler(string verb, HandlerRole role, Action<NetMessage, Action<string>> handler) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) Requests.RegisterRequestHandler(verb, role, handler); } public bool TryGetPendingRequest(string verb, out string token, out float secondsLeft) { if (_requests != null) { return _requests.TryGetPending(verb, out token, out secondsLeft); } token = null; secondsLeft = 0f; return false; } internal void RequestTick(float now) { _requests?.Tick(now); } internal void AbortRequestsOnRoomChange() { _requests?.DrainAllAsTimedOut(); } public bool IsPeerReady(int actor) { return _ready.Contains(actor); } public bool IsPeerSceneReady(int actor) { return _sceneReady.Contains(actor); } internal bool HasVerb(string verb) { return _verbs.ContainsKey(verb); } internal void Deliver(in NetMessage msg) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (_verbs.TryGetValue(msg.Verb ?? "", out var value)) { string text = RoleGuard.Evaluate(value.Role, PhotonNetwork.isMasterClient, msg.SenderIsMaster, msg.SenderIsSelf); if (text == null) { try { value.Handler(msg); return; } catch (Exception ex) { Counters.RecordDrop(msg.Verb, "handler-threw", (double)Time.unscaledTime); Plugin.Log.LogWarning((object)$"[{LogTag}] handler '{msg.Verb}' threw (actor {msg.SenderActor}): {ex}"); return; } } Counters.RecordDrop(msg.Verb, text, (double)Time.unscaledTime); } else { Counters.RecordDrop(msg.Verb, "no-handler", (double)Time.unscaledTime); if (Diagnostics.NoHandler.Record(Id, msg.Verb)) { Plugin.Log.LogWarning((object)("[NETKIT] no handler for verb '" + msg.Verb + "' on channel '" + Id + "' — " + $"dropped (actor {msg.SenderActor}) (further drops counted silently)")); } } } private void RaisePeerEvent(Action<PeerInfo> ev, string name, PeerInfo info) { EventFanout.Raise<PeerInfo>(ev, info, (Action<Delegate, Exception>)delegate(Delegate d, Exception e) { Plugin.Log.LogWarning((object)("[" + LogTag + "] an " + name + " subscriber threw " + $"({d.Method.DeclaringType?.FullName}.{d.Method.Name}): {e}")); }); } internal void MarkReady(int actor, PeerInfo info) { if (_ready.Add(actor)) { RaisePeerEvent(this.OnPeerReady, "OnPeerReady", info); } } internal void MarkLost(int actor, PeerInfo info) { _sceneReady.Remove(actor); if (_ready.Remove(actor)) { RaisePeerEvent(this.OnPeerLost, "OnPeerLost", info); } } internal bool SetSceneReady(int actor, bool ready) { if (!ready) { _sceneReady.Remove(actor); return false; } return _sceneReady.Add(actor); } internal void RaiseSceneReady(PeerInfo info) { RaisePeerEvent(this.OnPeerSceneReady, "OnPeerSceneReady", info); } internal void RaiseExtensionChanged(PeerInfo info) { RaisePeerEvent(this.OnPeerExtensionChanged, "OnPeerExtensionChanged", info); } internal void ResetPeers() { _ready.Clear(); _sceneReady.Clear(); } internal string HelloExtensionValue() { if (Options.HelloExtension == null) { return ""; } try { return Options.HelloExtension() ?? ""; } catch (Exception arg) { if (!_extensionThrewLogged) { _extensionThrewLogged = true; Plugin.Log.LogWarning((object)("[" + LogTag + "] the hello-extension provider for channel '" + Id + "' THREW — this channel's hello extension is now the fixed sentinel 'provider-threw', so peers " + $"see a changed extension once and then a stable one: {arg}")); } return "provider-threw"; } } internal string HeartbeatFragmentValue() { if (Options.HeartbeatFragment == null) { return ""; } try { return Options.HeartbeatFragment() ?? ""; } catch (Exception arg) { if (!_heartbeatThrewLogged) { _heartbeatThrewLogged = true; Plugin.Log.LogWarning((object)("[" + LogTag + "] the heartbeat-fragment provider for channel '" + Id + "' THREW — the " + string.Format("heartbeat line will carry '{0}' from here: {1}", "hb-frag-threw", arg))); } return "hb-frag-threw"; } } } [BepInPlugin("cobalt.netkit", "NetKit", "0.2.10")] [BepInDependency("cobalt.forgekit", "0.4.13")] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.netkit"; public const string NAME = "NetKit"; public const string VERSION = "0.2.10"; public const string COMPAT_SINCE = "0.2.4"; internal static ModLog Log = new ModLog(Logger.CreateLogSource("NetKit"), (LogTier)3); public static ConfigEntry<NetTransportKind> Transport; public static ConfigEntry<int> EventCode; public static ConfigEntry<float> HeartbeatSeconds; public static ConfigEntry<float> HelloWarnSeconds; public static ConfigEntry<float> HelloRefreshSeconds; public static ConfigEntry<bool> VerboseNet; public static ConfigEntry<int> DisconnectTimeoutMs; public static ConfigEntry<int> SimActor; public static ConfigEntry<int> SimDelayMs; public static ConfigEntry<int> SimJitterMs; public static ConfigEntry<int> SimDropPct; public static ConfigEntry<bool> SimOrdered; public static ConfigEntry<bool> SimEchoHello; public static ConfigEntry<bool> SimEchoAll; public static ConfigEntry<int> SimSeed; private CommandRegistry _commands; private CommandChannel _channel; [MethodImpl(MethodImplOptions.NoInlining)] private static void DeclareKitContracts() { KitContract.Declare("NetKit", "cobalt.forgekit", "0.4.13"); } private void TryDeclareKitContracts() { try { DeclareKitContracts(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[CONTRACT] kit handshake unavailable (" + ex.GetType().Name + ") — is ForgeKit older than this mod?")); } } internal void Awake() { //IL_0246: Unknown result type (might be due to invalid IL or missing references) //IL_024c: Expected O, but got Unknown //IL_02a7: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Expected O, but got Unknown //IL_02d4: Unknown result type (might be due to invalid IL or missing references) //IL_02d9: Unknown result type (might be due to invalid IL or missing references) //IL_02e4: Unknown result type (might be due to invalid IL or missing references) //IL_02ef: Unknown result type (might be due to invalid IL or missing references) //IL_02fa: Unknown result type (might be due to invalid IL or missing references) //IL_0311: Expected O, but got Unknown //IL_030c: Unknown result type (might be due to invalid IL or missing references) //IL_0316: Expected O, but got Unknown TryDeclareKitContracts(); Log = ModLog.Bind((BaseUnityPlugin)(object)this, ((BaseUnityPlugin)this).Logger); Transport = ((BaseUnityPlugin)this).Config.Bind<NetTransportKind>("Net", "Transport", NetTransportKind.Rpc, "Which Photon backend carries co-op traffic. Rpc (default) = the NK_Bus [PunRPC] relay piggybacked on CharacterManager's PhotonView (live-proven). Event = PhotonNetwork.RaiseEvent (one event code, no GameObject) — implemented + selftest-covered but NOT yet live-verified for its offline loopback / two-box behavior, so it stays opt-in until a spike clears it. Sim = DEV ONLY: no Photon traffic at all — a simulated wire + one fabricated remote actor (the [Net] Sim* settings; drive it with the `sim` verb). Never ship a profile with Sim set. Boot-time (transport is chosen once at Awake)."); EventCode = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "EventCode", 177, "The Photon custom event code used by the Event transport (0..199; 200+ are reserved). Only consulted when Transport=Event. Change only if it collides with another mod's RaiseEvent code."); HeartbeatSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Net", "HeartbeatSeconds", 30f, "Every N seconds while in a room, log one heartbeat line PER CHANNEL (role, peers ready, the consumer's fragment, PhotonNetwork.time) so a post-hoc log names when a session went quiet. 0 = off. Live via reloadcfg on the consumer side."); HelloWarnSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Net", "HelloWarnSeconds", 10f, "How long after a peer joins to wait for its nk.hello before warning it appears UNMODDED. The absence detector is what tells a modded peer from an unmodded one."); HelloRefreshSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Net", "HelloRefreshSeconds", 5f, "Every N seconds while in a room, compare the hello payload against the last one sent and RE-SEND it to peers when it changed — this is what carries a mid-session table retune (e.g. a host-side reloadtaming) to the other box with no scene change, so the handshake drift diff re-runs. Both roles. 0 = off (a retune then only rides the guest's next scene-ready re-hello, and a HOST-side retune never re-diffs). Boot-time."); VerboseNet = ((BaseUnityPlugin)this).Config.Bind<bool>("Net", "VerboseNet", true, "DEPRECATED — use [Diag] LogLevel (the wire trace rides the Verbose tier). Setting this false still suppresses every NK_Bus send/receive line regardless of LogLevel. netdump's counters + ring buffer are unaffected either way."); DisconnectTimeoutMs = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "DisconnectTimeoutMs", 0, "Override Photon's PeerBase.DisconnectTimeout (vanilla 10000 ms). At 10 s any peer silent for longer than that during a long additive/donor load, a GC hitch, or laggy Wi-Fi is torn down and re-read by our hello logic as 'guest went away / unmodded'. Widen to 30000-60000 to ride those out. 0 = leave vanilla (no patch applied — byte-identical install). Negative = invalid, ignored (vanilla). Boot-time (a getter patch applied once at Awake). TRADE-OFF: a genuinely dead peer now takes up to this window to be declared gone; solo unaffected."); SimActor = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "SimActor", 9, "Sim transport only: the fabricated remote peer's actor number. Keep it clear of real actor numbers (Photon assigns 1..N from 1) so log lines are unambiguous."); SimDelayMs = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "SimDelayMs", 120, "Sim transport only: base one-way latency of the simulated wire, in milliseconds."); SimJitterMs = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "SimJitterMs", 40, "Sim transport only: up to this many extra milliseconds of random jitter per envelope."); SimDropPct = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "SimDropPct", 0, "Sim transport only: percentage of envelopes the simulated wire loses (0..100). Photon reliable RPCs never drop in reality — nonzero is a hostile-wire stress model."); SimOrdered = ((BaseUnityPlugin)this).Config.Bind<bool>("Net", "SimOrdered", true, "Sim transport only: true (default) preserves send order like Photon's reliable-ordered RPCs; false lets jitter reorder deliveries (hostile-timing stress model)."); SimEchoHello = ((BaseUnityPlugin)this).Config.Bind<bool>("Net", "SimEchoHello", true, "Sim transport only: the sim peer answers an nk.hello with a same-bundle hello of its own, so every channel arms readiness toward it through the normal handshake."); SimEchoAll = ((BaseUnityPlugin)this).Config.Bind<bool>("Net", "SimEchoAll", false, "Sim transport only: the sim peer parrots EVERY envelope back verbatim as itself — a dumb fuzzer for handler idempotence/authorization. Distorts request protocols; off by default."); SimSeed = ((BaseUnityPlugin)this).Config.Bind<int>("Net", "SimSeed", 0, "Sim transport only: RNG seed for jitter/drop. 0 = derive from the clock and log it. Setting the logged value replays the same drop/jitter draw sequence (delivery timing still depends on frame cadence)."); Net.Init(Transport.Value, EventCode.Value); HelloService.Configure(HelloWarnSeconds.Value, HelloRefreshSeconds.Value); Diagnostics.Init(); Harmony val = new Harmony("cobalt.netkit"); val.PatchAll(typeof(NetKitPeerJoinedHook)); val.PatchAll(typeof(NetKitSelfJoinedHook)); val.PatchAll(typeof(NetKitPeerLeftHook)); TimeoutPatch.Configure(DisconnectTimeoutMs.Value); val.PatchAll(typeof(TimeoutPatch)); _commands = new CommandRegistry(ModLog.op_Implicit(Log)); RegisterVerbs(); _channel = new CommandChannel("NetKit_cmd.txt", ModLog.op_Implicit(Log), _commands, 0.5f, true, true, new CatalogInfo { ModGuid = "cobalt.netkit", ModName = "NetKit", ModVersion = "0.2.10", ConfigSource = () => ((BaseUnityPlugin)this).Config }); CommonVerbs.RegisterConfigVerbs(_commands, ModLog.op_Implicit(Log), (Func<ConfigFile>)(() => ((BaseUnityPlugin)this).Config), (Action)null, true); Log.LogMessage((object)(string.Format("{0} {1} loaded — transport {2}; ", "NetKit", "0.2.10", Transport.Value) + "'help' in BepInEx/config/NetKit_cmd.txt lists the verbs.")); Log.LogMessage((object)("[NETKIT] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); } internal void Update() { _channel.Tick(); Net.Tick(); (Net.Transport as SimTransport)?.Tick(); PoolPressure.Tick(); } private void RegisterVerbs() { _commands.Register("netdump", "Co-op census on THIS machine (both roles): transport/attach + per-channel verbs/peers/counters/ring buffer + hello ledger + PUN-signature counters + the unknown-view table.", (Action<string[]>)delegate { Log.LogMessage((object)Diagnostics.Dump()); }); _commands.Register("viewdump", "Photon view-registry census ('viewdump'): registered views + the manuallyAllocatedViewIds ledger (leak candidates / deferred corpse releases) as [PHOTON-RECON] lines — same body DonorKit's photondump forwards to (A10 re-homing 2026-08-15).", (Action<string[]>)delegate { ViewRegistryDump.Dump(Log.Sink); }); _commands.Register("selftest", "Run the NetKit self-test ([SELFTEST] PASS/FAIL … DONE): hello codec, compat calc, peer-ledger timing, counters, and — in a room — the transport loopback.", (Action<string[]>)delegate { SelfTest(); }); _commands.Register("netmute", "netmute [on|off|status] (bare = status). ON suppresses OUTGOING nk.hello sends so this box reads as UNMODDED to peers — the staging tool for incompatible-peer testplan rows. Incoming handling unchanged. Session-only (not persisted), default off.", (Action<string[]>)NetMute); _commands.Register("sim", "Sim-transport control ([Net] Transport=Sim only). 'sim status' = link + counters; 'sim join' = the sim peer introduces itself (injects its hello — channels arm readiness); 'sim leave' = the sim peer departs (peer-lost teardown); 'sim reset' = rebuild the wire from current [Net] Sim* config; 'sim send <channel> <verb> [payload…]' = inject one envelope AS the sim actor.", (Action<string[]>)SimVerb); } private void SimVerb(string[] args) { if (!(Net.Transport is SimTransport simTransport)) { Log.LogMessage((object)("[NETKIT] sim: transport is '" + (Net.Transport?.Name ?? "none") + "' — set [Net] Transport=Sim and relaunch to use the simulated peer.")); return; } simTransport.TryAttach(); switch (((args != null && args.Length > 1) ? args[1] : "").Trim().ToLowerInvariant()) { case "join": simTransport.InjectHello(); Log.LogMessage((object)($"[NETKIT] sim: hello injected from sim actor {simTransport.SimActor} — " + "channel readiness now follows the normal handshake (see netdump).")); break; case "leave": simTransport.Leave(); Log.LogMessage((object)$"[NETKIT] sim: actor {simTransport.SimActor} departed (peer-lost teardown raised)."); break; case "reset": simTransport.Rebuild(); Log.LogMessage((object)("[NETKIT] sim: wire rebuilt — " + simTransport.AttachInfo())); break; case "send": { if (args.Length < 4) { Log.LogMessage((object)"[NETKIT] sim send <channel> <verb> [payload…]"); break; } string payload = ((args.Length > 4) ? string.Join(" ", args, 4, args.Length - 4) : ""); simTransport.InjectEnvelope(args[2], args[3], "", payload); Log.LogMessage((object)$"[NETKIT] sim: injected '{args[2]}.{args[3]}' as actor {simTransport.SimActor}."); break; } default: Log.LogMessage((object)("[NETKIT] sim status: " + simTransport.AttachInfo())); break; } } private void NetMute(string[] args) { switch (((args != null && args.Length > 1) ? args[1] : "").Trim().ToLowerInvariant()) { case "on": case "true": case "1": if (!HelloService.HelloMuted) { HelloService.HelloMuted = true; Log.LogWarning((object)"[NETKIT] hello muted — this box now reads as UNMODDED to peers (netmute off to restore)"); } else { Log.LogMessage((object)"[NETKIT] netmute: already muted."); } break; case "off": case "false": case "0": if (HelloService.HelloMuted) { HelloService.HelloMuted = false; Log.LogMessage((object)"[NETKIT] hello UNMUTED — nk.hello sends restored (this box reads as MODDED again)."); } else { Log.LogMessage((object)"[NETKIT] netmute: already unmuted."); } break; default: Log.LogMessage((object)("[NETKIT] netmute status: hello is " + (HelloService.HelloMuted ? "MUTED (this box reads as UNMODDED to peers)" : "active (normal)") + ".")); break; } } private void SelfTest() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: 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_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Invalid comparison between Unknown and I4 //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Invalid comparison between Unknown and I4 //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Invalid comparison between Unknown and I4 //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Expected O, but got Unknown //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_0258: Unknown result type (might be due to invalid IL or missing references) //IL_025f: Expected O, but got Unknown SelfTestHarness val = new SelfTestHarness(ModLog.op_Implicit(Log)); val.Begin("NetKit"); try { HelloMsg val2 = new HelloMsg { Proto = 1, Channels = new List<ChannelHello> { new ChannelHello { Channel = "ck", Version = "0.4;0\\x", Extension = "hash=ab\ncd" }, new ChannelHello { Channel = "sk", Version = "0.3.0", Extension = "" } } }; HelloMsg val3 = val2; HelloMsg val4 = default(HelloMsg); bool flag = HelloCodec.TryDecode(HelloCodec.Encode(val3), ref val4); val.Check("hello codec round-trips (incl. ';'/'\\'/newline escaping)", flag && val4.Proto == val3.Proto && val4.Channels.Count == 2 && HelloCodec.VersionOf(ref val4, "ck") == "0.4;0\\x" && HelloCodec.ExtensionOf(ref val4, "ck") == "hash=ab\ncd" && HelloCodec.VersionOf(ref val4, "sk") == "0.3.0"); val.Check("hello codec refuses malformed payload", !HelloCodec.TryDecode("1;notacount", ref val2)); val.Check("compat: same proto+version = Compatible", (int)ChannelCompat.Evaluate(1, 1, "0.4.0", "0.4.0") == 0); val.Check("compat: proto mismatch", (int)ChannelCompat.Evaluate(1, 2, "0.4.0", "0.4.0") == 1); val.Check("compat: peer missing channel", (int)ChannelCompat.Evaluate(1, 1, "0.4.0", (string)null) == 3); PeerLedger val5 = new PeerLedger(); val5.Expect(7, 0.0); List<int> list = val5.DueForWarning(5.0, 10.0); List<int> list2 = val5.DueForWarning(11.0, 10.0); List<int> list3 = val5.DueForWarning(20.0, 10.0); val.Check("ledger: warns once past the window, then never again", list == null && list2 != null && list2.Count == 1 && list2[0] == 7 && list3 == null); val5.Reset(); val.Check("ledger: reset forgets everything", val5.PendingCount == 0 && val5.HelloedCount == 0); NetCounters val6 = new NetCounters(); val6.RecordSend("spawn", 1.0); val6.RecordRecv("spawn", 2.0); val6.RecordDrop("spawn", "no-handler", 3.0); val.Check("counters: send/recv/drop recorded", !val6.Empty && val6.TotalSends == 1 && val6.TotalRecvs == 1 && val6.TotalDrops == 1); int num = default(int); val.Check("pun: unknown-view id parsed from the PUN warn text", PunLog.TryParseViewId("Received OnSerialization for view ID 24007. We have no such PhotonView!", ref num) && num == 24007); val.Check("registry: internal 'nk' channel registered", Net.ChannelCount >= 1 && Net.TryGetChannel("nk", out var _)); if (PhotonNetwork.inRoom) { val.Check("transport: attached", Net.Attached || Net.Transport.TryAttach()); Net.LastTestPayload = null; bool flag2 = Net.Internal.SendToAllLoopback("nk.test", "loopback-proof"); val.Check("transport: NK_Bus loopback dispatched through the active transport", flag2 && Net.LastTestPayload == "loopback-proof"); } else { Log.LogMessage((object)"[SELFTEST] (not in a room — transport loopback skipped; load a save for the live check.)"); } } catch (Exception ex) { val.Exception(ex); } val.Done(); } } internal static class PoolPressure { private const float ProbeSeconds = 30f; private static float _probeAt; private static bool _warned; static PoolPressure() { Net.OnRoomChanged += delegate { _warned = false; }; } internal static void Tick() { if (Time.unscaledTime - _probeAt < 30f) { return; } _probeAt = Time.unscaledTime; if (_warned) { return; } int count; try { List<int> manuallyAllocatedViewIds = PhotonNetwork.manuallyAllocatedViewIds; if (manuallyAllocatedViewIds == null) { return; } count = manuallyAllocatedViewIds.Count; } catch { return; } if (LeasePolicy.ShouldWarnMintCount(count)) { _warned = true; Plugin.Log.LogWarning((object)($"[NETKIT] manual viewID pool pressure: {count} id(s) allocated this room " + "(hard-fail at 999) — something is allocating without releasing. Run 'viewdump' for the census; a scene change reclaims the pool, a relaunch is never required to read this.")); } } } public enum StoreAuthority { Master, Owner, PeerOwned } public sealed class StoreVerbs { public string Announce; public string Release; } public sealed class StoreOptions { public StoreAuthority Authority; public Func<string, int, int, OwnerResolution> ResolveUidOwner; public float RefreshSeconds = 30f; public bool FlushOnPeerReady = true; public bool ClearOnOwnerLost = true; public float OwnerMissingSeconds = 45f; public bool ClearOnRoomChange = true; public RebindRule RebindRule; public bool ReapAbsentActors; public StoreVerbs Verbs; } public struct RecordMeta { public int SenderActor; public bool IsRebind; public bool IsRefresh; public string Extra; } public sealed class ReplicatedStore { private struct PendingRelease { public string WireKey; public string Reason; } private readonly NetChannel _ch; private readonly StoreOptions _opt; private readonly RecordTable _table = new RecordTable(); private readonly AnnounceBook _book; private readonly Dictionary<string, PendingRelease> _pendingRelease = new Dictionary<string, PendingRelease>(StringComparer.Ordinal); private const float TickSeconds = 2f; private float _nextTickAt; public string Name { get; } private StoreAuthorityKind Kind => (StoreAuthorityKind)_opt.Authority; public int Count => _table.Count; public event Action<string, string, RecordMeta> OnSet; public event Action<string, string, RecordMeta> OnCleared; internal ReplicatedStore(NetChannel ch, string name, StoreOptions options) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Invalid comparison between Unknown and I4 //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Invalid comparison between Unknown and I4 //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Expected O, but got Unknown //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) _ch = ch; Name = name; _opt = options ?? new StoreOptions(); if (_opt.Verbs == null || string.IsNullOrEmpty(_opt.Verbs.Announce) || string.IsNullOrEmpty(_opt.Verbs.Release)) { throw new ArgumentException("store '" + name + "': StoreOptions.Verbs.Announce/Release are required (the wire bytes are the consumer's contract)."); } if ((int)Kind == 2 && !_opt.ClearOnRoomChange) { throw new ArgumentException("store '" + name + "': PeerOwned authority requires ClearOnRoomChange (its row key is DERIVED from the sender's ACTOR NUMBER, and actor numbers are scoped to a room — carrying rows across a room change leaves ghosts keyed to actors that mean someone else, or nobody, in the new room. The presence reap can't see it either: it compares actor numbers with no room identity. Refusing rather than shipping a store that silently rots."); } if ((int)Kind == 2 && _opt.ResolveUidOwner != null) { throw new ArgumentException("store '" + name + "': PeerOwned authority cannot take a ResolveUidOwner binding — its row key is DERIVED from the sender actor, so there is no claimed uid left to authorize. Refusing rather than silently ignoring the callback."); } _book = new AnnounceBook((double)_opt.RefreshSeconds); HandlerRole role = StoreRoles.For(Kind); _ch.Register(_opt.Verbs.Announce, OnAnnounceMsg, role); _ch.Register(_opt.Verbs.Release, OnReleaseMsg, role); _ch.OnPeerLost += delegate(PeerInfo info) { ClearActorRows(info.Actor, $"owner (actor {info.Actor}) disconnected"); }; _ch.OnPeerReady += OnPeerReady; _ch.OnPeerSceneReady += OnPeerSceneReady; Net.OnRoomChanged += OnRoomChanged; } public bool Announce(string key, string payload, string extra = "") { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 if (payload == null) { return false; } if (StoreRoles.KeyFromSender(Kind) && (!Net.InRoom || LocalActor() == 0)) { return false; } double num = Time.unscaledTime; MirrorDecision val = _book.Decide(key, payload, extra, num); if ((int)val != 1 && (int)val != 2) { return true; } string text = default(string); int num2 = default(int); RecordKey.TryParse(key, ref text, ref num2); string text2 = RecordKey.Wire(text, num2); string extra2 = RecordKey.EncodeExtraSlot(text2, extra); bool flag = StoreRoles.BroadcastsToOthers(Kind); if (flag) { ApplyLocal(text2, payload, extra, num); } if (!(flag ? _ch.SendToOthers(_opt.Verbs.Announce, payload, extra2) : _ch.SendToMaster(_opt.Verbs.Announce, payload, extra2))) { return false; } _book.OnLanded(key, payload, extra, num); _pendingRelease.Remove(RecordKey.Canonical(key)); return true; } public bool Release(string key, string reason = "") { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) string text = default(string); int num = default(int); RecordKey.TryParse(key, ref text, ref num); string wireKey = RecordKey.Wire(text, num); _book.Forget(key); if (StoreRoles.KeyFromSender(Kind) && (!Net.InRoom || LocalActor() == 0)) { return false; } RecordRow val = default(RecordRow); if (StoreRoles.BroadcastsToOthers(Kind) && _table.Clear(SenderKey(wireKey, LocalActor()), ref val)) { RaiseCleared(val.Key, (reason.Length > 0) ? reason : "released by owner", new RecordMeta { SenderActor = LocalActor(), Extra = val.Extra }); } bool flag = SendRelease(wireKey, reason); if (!flag && Net.InRoom) { _pendingRelease[RecordKey.Canonical(key)] = new PendingRelease { WireKey = wireKey, Reason = reason }; } else { _pendingRelease.Remove(RecordKey.Canonical(key)); } return flag; } public void Invalidate(string key) { _book.Invalidate(key); } public bool TryGet(string key, out RecordRow row) { return _table.TryGet(key, ref row); } public List<RecordRow> RowsSnapshot() { return _table.RowsSnapshot(); } public bool ClearLocal(string key, string reason) { RecordRow val = default(RecordRow); if (!_table.Clear(key, ref val)) { return false; } RaiseCleared(val.Key, reason ?? "cleared", new RecordMeta { SenderActor = val.ActorId, Extra = val.Extra }); return true; } private void OnAnnounceMsg(NetMessage msg) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Expected I4, but got Unknown //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0241: Invalid comparison between Unknown and I4 //IL_02b3: Unknown result type (might be due to invalid IL or missing references) //IL_02b5: Invalid comparison between Unknown and I4 //IL_034b: Unknown result type (might be due to invalid IL or missing references) //IL_034d: Invalid comparison between Unknown and I4 //IL_0356: Unknown result type (might be due to invalid IL or missing references) //IL_0358: Invalid comparison between Unknown and I4 string text = default(string); string wireKey = default(string); RecordKey.DecodeExtraSlot(msg.Extra, ref wireKey, ref text); wireKey = SenderKey(wireKey, msg.SenderActor); Func<string, int, int, OwnerResolution> func = (StoreRoles.OwnerBindingApplies(Kind) ? _opt.ResolveUidOwner : null); RecordRow val2 = default(RecordRow); RecordApply val = _table.Apply(wireKey, msg.Payload, text, msg.SenderActor, (double)Time.unscaledTime, (Func<int, bool>)ActorPresent, func, ref val2); switch ((int)val) { case 0: _ch.CountDrop(_opt.Verbs.Announce, "empty-key"); return; case 1: _ch.CountDrop(_opt.Verbs.Announce, "owner-unresolvable"); Plugin.Log.LogMessage((object)("[" + _ch.LogTag + "] store '" + Name + "': announce for '" + wireKey + "' (actor " + $"{msg.SenderActor}) DEFERRED — owner unresolvable; binding waits for the re-announce.")); return; case 2: _ch.CountDrop(_opt.Verbs.Announce, "not-owner"); Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] store '" + Name + "': announce for '" + wireKey + "' REFUSED — " + $"sender actor {msg.SenderActor} does not own that uid.")); return; case 3: _ch.CountDrop(_opt.Verbs.Announce, "rebind-refused"); Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] store '" + Name + "': announce for '" + wireKey + "' REFUSED — " + $"sender actor {msg.SenderActor} tried to rebind a row whose bound actor is still in the room.")); return; } if ((int)val == 4) { Plugin.Log.LogMessage((object)("[" + _ch.LogTag + "] store '" + Name + "': row '" + val2.Key + "' created " + $"(actor {msg.SenderActor}).")); } else if ((int)val == 7) { Plugin.Log.LogMessage((object)("[" + _ch.LogTag + "] store '" + Name + "': row '" + val2.Key + "' REBOUND to actor " + $"{msg.SenderActor} (previous owner left the room — reconnect).")); } RaiseSet(val2.Key, val2.Payload, new RecordMeta { SenderActor = msg.SenderActor, IsRebind = ((int)val == 7), IsRefresh = ((int)val == 6), Extra = val2.Extra }); } private void OnReleaseMsg(NetMessage msg) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Invalid comparison between Unknown and I4 string text = default(string); string wireKey = default(string); RecordKey.DecodeExtraSlot(msg.Extra, ref wireKey, ref text); wireKey = SenderKey(wireKey, msg.SenderActor); RecordRow val2 = default(RecordRow); RecordRelease val = _table.Release(wireKey, msg.SenderActor, ref val2); if ((int)val != 0) { if ((int)val == 1) { _ch.CountDrop(_opt.Verbs.Release, "wrong-sender"); Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] store '" + Name + "': release for '" + wireKey + "' REFUSED — " + $"sender actor {msg.SenderActor} is not the bound owner actor.")); } else { string reason = (string.IsNullOrEmpty(msg.Payload) ? "released by owner" : ("owner release: " + msg.Payload)); RaiseCleared(val2.Key, reason, new RecordMeta { SenderActor = msg.SenderActor, Extra = val2.Extra }); } } else { _ch.CountDrop(_opt.Verbs.Release, "no-row"); } } private void OnPeerReady(PeerInfo info) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) if (_opt.FlushOnPeerReady) { if (!StoreRoles.BroadcastsToOthers(Kind)) { _book.InvalidateAll(); } else if (!_ch.IsPeerSceneReady(info.Actor)) { Plugin.Log.LogMessage((object)($"[{_ch.LogTag}] store '{Name}': actor {info.Actor} is channel-ready but " + "still LOADING — flush deferred to its scene-ready hello (N-3; a stuck load leaves this as the last line for that actor).")); } } } private void OnPeerSceneReady(PeerInfo info) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (_opt.FlushOnPeerReady && StoreRoles.BroadcastsToOthers(Kind)) { int num = FlushTo(info.Actor); if (num > 0) { Plugin.Log.LogMessage((object)($"[{_ch.LogTag}] store '{Name}': flushed {num} record(s) to actor " + $"{info.Actor} (peer scene-ready — set is idempotent).")); } } } public int FlushTo(int actor, Action<string> onLanded = null) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0046: 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_0052: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) if (!StoreRoles.BroadcastsToOthers(Kind)) { return 0; } int num = 0; foreach (AnnounceEntry item in _book.FlushSnapshot()) { if (!_ch.SendToActor(actor, _opt.Verbs.Announce, item.Payload, RecordKey.EncodeExtraSlot(item.WireKey, item.Extra))) { continue; } num++; if (onLanded != null) { try { onLanded(item.WireKey); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] store '" + Name + "': FlushTo onLanded callback threw for '" + item.WireKey + "': " + ex.Message)); } } } return num; } private void OnRoomChanged(RoomChange change) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) _book.Reset(); _pendingRelease.Clear(); if (!_opt.ClearOnRoomChange) { return; } string reason = "room changed ('" + (change.OldRoomName ?? "none") + "' → '" + (change.NewRoomName ?? "none") + "')"; foreach (RecordRow item in _table.ClearAll()) { RaiseCleared(item.Key, reason, new RecordMeta { SenderActor = item.ActorId, Extra = item.Extra }); } } private void ClearActorRows(int actorId, string reason) { foreach (RecordRow item in _table.ClearActor(actorId)) { RaiseCleared(item.Key, reason, new RecordMeta { SenderActor = actorId, Extra = item.Extra }); } } internal void Tick(float now) { if (!(now < _nextTickAt)) { _nextTickAt = now + 2f; RetryPendingReleases(); OwnerMissingBackstop(now); PresenceReap(now); } } private void PresenceReap(float now) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (!_opt.ReapAbsentActors || !StoreRoles.BroadcastsToOthers(Kind) || _table.Count == 0 || !Net.InRoom) { return; } int num = LocalActor(); List<RecordRow> list = null; foreach (RecordRow item in _table.RowsSnapshot()) { if (item.ActorId != num && !ActorPresent(item.ActorId)) { (list ?? (list = new List<RecordRow>())).Add(item); } } if (list == null) { return; } RecordRow val = default(RecordRow); foreach (RecordRow item2 in list) { if (_table.Clear(item2.Key, ref val)) { RaiseCleared(val.Key, $"actor {val.ActorId} no longer in room (presence reap)", new RecordMeta { SenderActor = val.ActorId, Extra = val.Extra }); } } } private void RetryPendingReleases() { if (_pendingRelease.Count == 0) { return; } if (!Net.InRoom) { _pendingRelease.Clear(); return; } List<string> list = null; foreach (KeyValuePair<string, PendingRelease> item in _pendingRelease) { if (SendRelease(item.Value.WireKey, item.Value.Reason)) { (list ?? (list = new List<string>())).Add(item.Key); } } if (list == null) { return; } foreach (string item2 in list) { _pendingRelease.Remove(item2); } } private void OwnerMissingBackstop(float now) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Invalid comparison between Unknown and I4 //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Invalid comparison between Unknown and I4 if (!StoreRoles.OwnerBindingApplies(Kind) || !_opt.ClearOnOwnerLost || _opt.ResolveUidOwner == null || _table.Count == 0 || !Net.IsMaster) { return; } List<RecordRow> list = null; foreach (RecordRow item in _table.RowsSnapshot()) { bool flag; try { flag = (int)_opt.ResolveUidOwner(item.OwnerUid, item.Slot, item.ActorId) > 0; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] store '" + Name + "': owner resolver threw for '" + item.Key + "': " + ex.Message + " — treated as resolvable this tick.")); flag = true; } if ((int)_table.TickRow(item, flag, (double)now, (double)_opt.OwnerMissingSeconds) == 1) { (list ?? (list = new List<RecordRow>())).Add(item); } } if (list == null) { return; } RecordRow val = default(RecordRow); foreach (RecordRow item2 in list) { if (_table.Clear(item2.Key, ref val)) { RaiseCleared(val.Key, $"owner unresolvable for {_opt.OwnerMissingSeconds:F0}s (backstop)", new RecordMeta { SenderActor = val.ActorId, Extra = val.Extra }); } } } private bool SendRelease(string wireKey, string reason) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (!StoreRoles.BroadcastsToOthers(Kind)) { return _ch.SendToMaster(_opt.Verbs.Release, reason ?? "", wireKey); } return _ch.SendToOthers(_opt.Verbs.Release, reason ?? "", wireKey); } private string SenderKey(string wireKey, int actor) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (!StoreRoles.KeyFromSender(Kind)) { return wireKey; } string text = default(string); int num = default(int); RecordKey.TryParse(wireKey, ref text, ref num); return RecordKey.Wire(actor.ToString(CultureInfo.InvariantCulture), num); } private void ApplyLocal(string wireKey, string payload, string extra, double now) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 int num = LocalActor(); RecordRow val2 = default(RecordRow); RecordApply val = _table.Apply(SenderKey(wireKey, num), payload, extra, num, now, (Func<int, bool>)ActorPresent, (Func<string, int, int, OwnerResolution>)null, ref val2); if (val2 != null) { RaiseSet(val2.Key, val2.Payload, new RecordMeta { SenderActor = num, IsRefresh = ((int)val == 6), Extra = val2.Extra }); } } private static int LocalActor() { try { return (PhotonNetwork.player != null) ? PhotonNetwork.player.ID : 0; } catch { return 0; } } private static bool ActorPresent(int actorId) { try { return PhotonPlayer.Find(actorId) != null; } catch { return false; } } private void RaiseSet(string key, string payload, RecordMeta meta) { Action<string, string, RecordMeta> action = this.OnSet; if (action == null) { return; } try { action(key, payload, meta); } catch (Exception ex) { _ch.CountDrop(_opt.Verbs.Announce, "callback-threw"); Plugin.Log.LogWarning((object)$"[{_ch.LogTag}] store '{Name}': an OnSet subscriber threw for '{key}': {ex}"); } } private void RaiseCleared(string key, string reason, RecordMeta meta) { Action<string, string, RecordMeta> action = this.OnCleared; if (action == null) { return; } try { action(key, reason, meta); } catch (Exception ex) { _ch.CountDrop(_opt.Verbs.Release, "callback-threw"); Plugin.Log.LogWarning((object)$"[{_ch.LogTag}] store '{Name}': an OnCleared subscriber threw for '{key}': {ex}"); } } internal string Dump() { //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) float unscaledTime = Time.unscaledTime; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append($"[{_ch.LogTag}] store '{Name}' authority={_opt.Authority} rows={_table.Count} " + $"announces={_book.Count} pendingReleases={_pendingRelease.Count} " + string.Format("refresh={0:F0}s binding={1}", _opt.RefreshSeconds, (_opt.ResolveUidOwner != null) ? "on" : "off") + ((StoreRoles.BroadcastsToOthers(Kind) && _opt.ReapAbsentActors) ? " reap=on" : "")); bool flag = StoreRoles.BroadcastsToOthers(Kind) && _opt.ReapAbsentActors; int num = (flag ? LocalActor() : 0); foreach (RecordRow item in _table.RowsSnapshot()) { double num2 = (double)unscaledTime - item.LastSetAt; bool flag2 = _opt.RefreshSeconds > 0f && num2 > (double)_opt.RefreshSeconds * 2.5; string text = ((item.OwnerMissingSince > 0.0) ? $" ownerMissing={(double)unscaledTime - item.OwnerMissingSince:F0}s" : ""); stringBuilder.AppendLine(); string text2 = (flag ? (" present=" + ((item.ActorId == num || ActorPresent(item.ActorId)) ? "yes" : "no")) : ""); stringBuilder.Append($"[{_ch.LogTag}] row '{item.Key}' actor={item.ActorId} age={num2:F0}s " + (flag2 ? "STALE" : "fresh") + text2 + text + ((item.Extra.Length > 0) ? (" extra='" + item.Extra + "'") : "")); } return stringBuilder.ToString(); } } public enum RequestOutcome { Ok, TimedOut, RefusedNoSend } public struct RequestResult { public RequestOutcome Outcome; public string Result; public string Token; public string Refusal; public const string ReasonInFlight = "in-flight"; public const string ReasonSendFailed = "send-failed"; public static RequestResult Answered(string token, string result) { return new RequestResult { Outcome = RequestOutcome.Ok, Token = (token ?? ""), Result = (result ?? "") }; } public static RequestResult Expired(string token) { return new RequestResult { Outcome = RequestOutcome.TimedOut, Token = (token ?? "") }; } public static RequestResult Refused(string refusal, string token = "") { return new RequestResult { Outcome = RequestOutcome.RefusedNoSend, Token = (token ?? ""), Refusal = (refusal ?? "") }; } } internal sealed class RequestHub { private const string AckSuffix = ".ack"; private readonly NetChannel _ch; private readonly RequestBook _book = new RequestBook(); private readonly Dictionary<string, Action<RequestResult>> _callbacks = new Dictionary<string, Action<RequestResult>>(StringComparer.Ordinal); private readonly HashSet<string> _ackRoutes = new HashSet<string>(StringComparer.Ordinal); internal RequestHub(NetChannel ch) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown _ch = ch; } internal static string AckVerbFor(string verb) { return verb + ".ack"; } internal bool SendRequest(string verb, string payload, float timeoutSeconds, Action<RequestResult> onResult, string extra, bool singleFlight) { if (string.IsNullOrEmpty(verb) || onResult == null) { return false; } EnsureAckRoute(verb); int num = ((PhotonNetwork.player != null) ? PhotonNetwork.player.ID : 0); string refusal = default(string); string text = _book.Begin(verb, num, (double)Time.unscaledTime, (double)timeoutSeconds, singleFlight, ref refusal); if (text == null) { Invoke(onResult, RequestResult.Refused(refusal), verb); return false; } _callbacks[text] = onResult; if (!_ch.SendToMaster(verb, RequestCodec.EncodeRequest(text, payload), extra ?? "")) { _callbacks.Remove(text); _book.Abort(text); Invoke(onResult, RequestResult.Refused("send-failed", text), verb); return false; } return true; } private void EnsureAckRoute(string verb) { if (_ackRoutes.Add(verb)) { string ackVerb = AckVerbFor(verb); _ch.Register(ackVerb, delegate(NetMessage msg) { OnAck(ackVerb, msg); }, (HandlerRole)4); } } private void OnAck(string ackVerb, NetMessage msg) { string text = default(string); string result = default(string); if (!RequestCodec.TryParseAck(msg.Payload, ref text, ref result)) { _ch.CountDrop(ackVerb, "unparseable"); return; } string text2 = default(string); if (!_book.TryResolve(text, ref text2)) { _ch.CountDrop(ackVerb, "stale-token"); return; } if (_callbacks.TryGetValue(text, out var value)) { _callbacks.Remove(text); Invoke(value, RequestResult.Answered(text, result), text2); return; } _ch.CountDrop(ackVerb, "ack-no-callback"); Plugin.Log.LogWarning((object)("[" + _ch.LogTag + "] request '" + text2 + "' ack (token " + text + ") resolved but NO callback was registered — the requester will never be told. This should be impossible; please report it.")); } internal void Tick(float now) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) if (_book.PendingCount == 0) { return; } List<Expired> list = _book.TakeExpired((double)now); if (list == null) { return; } foreach (Expired item in list) { _ch.CountDrop(item.Verb, "request-timeout"); if (_callbacks.TryGetValue(item.Token, out var value)) { _callbacks.Remove(item.Token); Invoke(value, RequestResult.Expired(item.Token), item.Verb); } } } internal void DrainAllAsTimedOut() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing r