Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of BreakoutNet v0.2.1
BreakoutNet.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Logging; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: AssemblyCompany("BreakoutMods")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("BreakoutMods Valheim client/server networking helper API.")] [assembly: AssemblyFileVersion("0.2.1.0")] [assembly: AssemblyInformationalVersion("0.2.1")] [assembly: AssemblyProduct("BreakoutNet")] [assembly: AssemblyTitle("BreakoutNet")] [assembly: AssemblyVersion("0.2.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 BreakoutMods.BreakoutNet { public sealed class BreakoutNetworkReadyEvent : IBreakoutEvent { public bool IsServer { get; } public bool IsClient { get; } public bool IsDedicatedServer { get; } public bool IsListenServer { get; } public BreakoutNetworkReadyEvent(bool isServer, bool isClient, bool isDedicatedServer, bool isListenServer) { IsServer = isServer; IsClient = isClient; IsDedicatedServer = isDedicatedServer; IsListenServer = isListenServer; } } public sealed class BreakoutWorldLeftEvent : IBreakoutEvent { } public sealed class BreakoutPeerChangedEvent : IBreakoutEvent { public long PeerId { get; } public string PlayerName { get; } public bool IsServerPeer { get; } public bool IsLocalPeer { get; } public BreakoutPeerChangedEvent(long peerId, string playerName, bool isServerPeer, bool isLocalPeer) { PeerId = peerId; PlayerName = playerName ?? string.Empty; IsServerPeer = isServerPeer; IsLocalPeer = isLocalPeer; } } public sealed class BreakoutRpcObservedEvent : IBreakoutEvent { public long SenderPeerId { get; } public bool IsFromServer { get; } public bool IsServerSide { get; } public string RpcName { get; } public string SenderModGuid { get; } public string MessageTypeName { get; } public int Sequence { get; } public BreakoutRpcObservedEvent(long senderPeerId, bool isFromServer, bool isServerSide, string rpcName, string senderModGuid, string messageTypeName, int sequence) { SenderPeerId = senderPeerId; IsFromServer = isFromServer; IsServerSide = isServerSide; RpcName = rpcName ?? string.Empty; SenderModGuid = senderModGuid ?? string.Empty; MessageTypeName = messageTypeName ?? string.Empty; Sequence = sequence; } } public sealed class BreakoutRpcRejectedEvent : IBreakoutEvent { public long SenderPeerId { get; } public string RpcName { get; } public string Reason { get; } public string Category { get; } public BreakoutRpcRejectedEvent(long senderPeerId, string rpcName, string reason, string category) { SenderPeerId = senderPeerId; RpcName = rpcName ?? string.Empty; Reason = reason ?? string.Empty; Category = category ?? "unknown"; } } public sealed class BreakoutEvents { private readonly string modGuid; private readonly BreakoutModApp owner; internal BreakoutEvents(string modGuid, BreakoutModApp owner) { this.modGuid = modGuid; this.owner = owner; } public BreakoutSubscription Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class, IBreakoutEvent { return Track(BreakoutEventRegistry.SubscribeScoped(modGuid, handler)); } public BreakoutSubscription Subscribe<TEvent>(string eventName, Action<TEvent> handler) where TEvent : class, IBreakoutEvent { return Track(BreakoutEventRegistry.SubscribeNamed(modGuid, eventName, handler)); } public void Publish<TEvent>(TEvent evt) where TEvent : class, IBreakoutEvent { BreakoutEventRegistry.PublishScoped(modGuid, evt); } public void Publish<TEvent>(string eventName, TEvent evt) where TEvent : class, IBreakoutEvent { BreakoutEventRegistry.PublishNamed(modGuid, eventName, evt); } private BreakoutSubscription Track(BreakoutSubscription subscription) { if (owner != null) { owner.Track(subscription); } return subscription; } } public sealed class BreakoutHooks { private readonly BreakoutModApp owner; internal BreakoutHooks(string modGuid, BreakoutModApp owner) { this.owner = owner; } public BreakoutSubscription OnNetworkReady(Action<BreakoutNetworkReadyEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe(handler)); } public BreakoutSubscription OnWorldLeft(Action<BreakoutWorldLeftEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe(handler)); } public BreakoutSubscription OnPeerJoined(Action<BreakoutPeerChangedEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe("breakoutnet.core.peer.joined", handler)); } public BreakoutSubscription OnPeerLeft(Action<BreakoutPeerChangedEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe("breakoutnet.core.peer.left", handler)); } public BreakoutSubscription OnRpcReceived(Action<BreakoutRpcObservedEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe(handler)); } public BreakoutSubscription OnRpcRejected(Action<BreakoutRpcRejectedEvent> handler) { return Track(BreakoutCoreHookRegistry.Subscribe(handler)); } private BreakoutSubscription Track(BreakoutSubscription subscription) { if (owner != null) { owner.Track(subscription); } return subscription; } } public sealed class BreakoutModApp : IDisposable { private readonly List<IBreakoutModule> modules = new List<IBreakoutModule>(); private readonly List<BreakoutSubscription> subscriptions = new List<BreakoutSubscription>(); private bool disposed; public BreakoutModuleContext Context { get; } public string ModGuid => Context.ModGuid; internal BreakoutModApp(string modGuid) { Context = new BreakoutModuleContext(modGuid, this); } internal void AddModule(IBreakoutModule module) { modules.Add(module); } internal void Track(BreakoutSubscription subscription) { if (subscription != null) { subscriptions.Add(subscription); } } public void Dispose() { if (disposed) { return; } disposed = true; BreakoutSubscription[] array = subscriptions.ToArray(); for (int i = 0; i < array.Length; i++) { array[i].Dispose(); } IBreakoutModule[] array2 = modules.ToArray(); for (int i = 0; i < array2.Length; i++) { if (array2[i] is IDisposable disposable) { disposable.Dispose(); } } subscriptions.Clear(); modules.Clear(); } } public sealed class BreakoutModBuilder { private readonly BaseUnityPlugin plugin; private readonly string modGuid; private readonly List<Type> sharedModules = new List<Type>(); private readonly List<Type> serverModules = new List<Type>(); private readonly List<Type> clientModules = new List<Type>(); internal BreakoutModBuilder(BaseUnityPlugin plugin, string modGuid) { if (string.IsNullOrWhiteSpace(modGuid)) { throw new ArgumentException("Mod GUID cannot be empty.", "modGuid"); } this.plugin = plugin; this.modGuid = modGuid; } public BreakoutModBuilder AddShared<TModule>() where TModule : IBreakoutModule, new() { sharedModules.Add(typeof(TModule)); return this; } public BreakoutModBuilder AddServer<TModule>() where TModule : IBreakoutModule, new() { serverModules.Add(typeof(TModule)); return this; } public BreakoutModBuilder AddClient<TModule>() where TModule : IBreakoutModule, new() { clientModules.Add(typeof(TModule)); return this; } public BreakoutModApp Build() { BreakoutModApp breakoutModApp = new BreakoutModApp(modGuid); BreakoutModuleContext context = breakoutModApp.Context; CreateModules(sharedModules, context, breakoutModApp); CreateModules(serverModules, context, breakoutModApp); if (!Application.isBatchMode) { CreateModules(clientModules, context, breakoutModApp); } BreakoutModAppOwner breakoutModAppOwner = ((Component)plugin).gameObject.GetComponent<BreakoutModAppOwner>(); if ((Object)(object)breakoutModAppOwner == (Object)null) { breakoutModAppOwner = ((Component)plugin).gameObject.AddComponent<BreakoutModAppOwner>(); } breakoutModAppOwner.Add(breakoutModApp); return breakoutModApp; } private static void CreateModules(IEnumerable<Type> moduleTypes, BreakoutModuleContext context, BreakoutModApp app) { foreach (Type moduleType in moduleTypes) { IBreakoutModule breakoutModule = (IBreakoutModule)Activator.CreateInstance(moduleType); breakoutModule.Initialize(context); app.AddModule(breakoutModule); } } } public abstract class BreakoutModuleBase : IBreakoutModule { protected BreakoutModuleContext Context { get; private set; } public virtual void Initialize(BreakoutModuleContext context) { Context = context; } } public abstract class BreakoutSharedModule : BreakoutModuleBase { } public abstract class BreakoutServerModule : BreakoutModuleBase { } public abstract class BreakoutClientModule : BreakoutModuleBase { } public sealed class BreakoutModuleContext { public string ModGuid { get; } public BreakoutEvents Events { get; } public BreakoutHooks Hooks { get; } internal BreakoutModApp Owner { get; } internal BreakoutModuleContext(string modGuid, BreakoutModApp owner) { if (string.IsNullOrWhiteSpace(modGuid)) { throw new ArgumentException("Mod GUID cannot be empty.", "modGuid"); } ModGuid = modGuid; Owner = owner; Events = new BreakoutEvents(modGuid, owner); Hooks = new BreakoutHooks(modGuid, owner); } } public static class BreakoutNet { public static BreakoutModuleContext ForMod(string modGuid) { return new BreakoutModuleContext(modGuid, null); } public static BreakoutModBuilder ForPlugin(BaseUnityPlugin plugin, string modGuid) { if ((Object)(object)plugin == (Object)null) { throw new ArgumentNullException("plugin"); } return new BreakoutModBuilder(plugin, modGuid); } } public static class BreakoutPeers { private static readonly IReadOnlyList<ZNetPeer> EmptyPeers = new List<ZNetPeer>(); public static ZNetPeer ServerPeer { get { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return null; } return ZNet.instance.GetServerPeer(); } } public static IReadOnlyList<ZNetPeer> ConnectedPeers { get { if ((Object)(object)ZNet.instance == (Object)null) { return EmptyPeers; } return ZNet.instance.GetConnectedPeers(); } } public static bool TryGetPeer(long peerId, out ZNetPeer peer) { peer = null; if ((Object)(object)ZNet.instance == (Object)null) { return false; } ZNetPeer serverPeer = ServerPeer; if (serverPeer != null && serverPeer.m_uid == peerId) { peer = serverPeer; return true; } foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers()) { if (connectedPeer != null && connectedPeer.m_uid == peerId) { peer = connectedPeer; return true; } } return false; } } public static class BreakoutRpc { public static class Server { public static void Register<TRequest>(string rpcName, BreakoutRpcHandler<TRequest> handler) where TRequest : IBreakoutSerializable, new() { BreakoutRpcRegistry.RegisterServer(rpcName, handler, BreakoutRpcRateLimit.Default); } public static void Register<TRequest>(string rpcName, BreakoutRpcHandler<TRequest> handler, BreakoutRpcRateLimit rateLimit) where TRequest : IBreakoutSerializable, new() { BreakoutRpcRegistry.RegisterServer(rpcName, handler, rateLimit); } public static bool SendToClient<TMessage>(long peerId, string rpcName, TMessage message, string senderModGuid = null) where TMessage : IBreakoutSerializable, new() { return BreakoutRpcRegistry.SendToPeer(peerId, rpcName, message, senderModGuid); } public static bool Broadcast<TMessage>(string rpcName, TMessage message, string senderModGuid = null) where TMessage : IBreakoutSerializable, new() { return BreakoutRpcRegistry.Broadcast(rpcName, message, senderModGuid); } public static bool BroadcastToAll<TMessage>(string rpcName, TMessage message, string senderModGuid = null) where TMessage : IBreakoutSerializable, new() { return Broadcast(rpcName, message, senderModGuid); } public static int BroadcastExcept<TMessage>(long senderPeerId, string rpcName, TMessage message, string senderModGuid = null) where TMessage : IBreakoutSerializable, new() { return BreakoutRpcRegistry.BroadcastExcept(senderPeerId, rpcName, message, senderModGuid); } public static int BroadcastNear<TMessage>(Vector3 position, float radius, string rpcName, TMessage message, string senderModGuid = null) where TMessage : IBreakoutSerializable, new() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return BreakoutRpcRegistry.BroadcastNear(position, radius, rpcName, message, senderModGuid); } } public static class Client { public static void Register<TMessage>(string rpcName, BreakoutRpcHandler<TMessage> handler) where TMessage : IBreakoutSerializable, new() { BreakoutRpcRegistry.RegisterClient(rpcName, handler); } public static bool SendToServer<TRequest>(string rpcName, TRequest request, string senderModGuid = null) where TRequest : IBreakoutSerializable, new() { return BreakoutRpcRegistry.SendToServer(rpcName, request, senderModGuid); } } } public sealed class BreakoutRpcContext { public long SenderPeerId { get; } public bool IsFromServer { get; } public bool IsServerSide { get; } public string RpcName { get; } public string SenderModGuid { get; } public int Sequence { get; } public bool IsRejected { get; private set; } public string RejectionReason { get; private set; } internal BreakoutRpcContext(long senderPeerId, bool isFromServer, bool isServerSide, string rpcName, string senderModGuid, int sequence) { SenderPeerId = senderPeerId; IsFromServer = isFromServer; IsServerSide = isServerSide; RpcName = rpcName; SenderModGuid = senderModGuid; Sequence = sequence; } public void Reject(string reason) { IsRejected = true; RejectionReason = (string.IsNullOrWhiteSpace(reason) ? "Rejected by handler." : reason); BreakoutLog.Warning("Rejected RPC '{0}' from peer {1}: {2}", RpcName, SenderPeerId, RejectionReason); } } public delegate void BreakoutRpcHandler<TMessage>(BreakoutRpcContext context, TMessage message) where TMessage : IBreakoutSerializable, new(); public sealed class BreakoutRpcRateLimit { private static readonly BreakoutRpcRateLimit DefaultPolicy = new BreakoutRpcRateLimit(enabled: true, 12f, 8f); private static readonly BreakoutRpcRateLimit UnlimitedPolicy = new BreakoutRpcRateLimit(enabled: false, 0f, 0f); public bool Enabled { get; private set; } public float Capacity { get; private set; } public float RefillPerSecond { get; private set; } public static BreakoutRpcRateLimit Default => DefaultPolicy; public static BreakoutRpcRateLimit Unlimited => UnlimitedPolicy; private BreakoutRpcRateLimit(bool enabled, float capacity, float refillPerSecond) { Enabled = enabled; Capacity = Math.Max(1f, capacity); RefillPerSecond = Math.Max(0f, refillPerSecond); } public static BreakoutRpcRateLimit TokenBucket(float capacity, float refillPerSecond) { return new BreakoutRpcRateLimit(enabled: true, capacity, refillPerSecond); } public static BreakoutRpcRateLimit ForMessagesPerSecond(float messagesPerSecond, float burstSeconds) { float num = Math.Max(1f, messagesPerSecond); float num2 = Math.Max(1f, burstSeconds); return TokenBucket(num * num2, num); } } public static class BreakoutSettingsSync { public static class Client { public static void Register<TSettings>(string settingsName, Action<TSettings> applySettings) where TSettings : IBreakoutSerializable, new() { BreakoutSettingsSyncRegistry.RegisterClientSettings(settingsName, applySettings); } } public static void RegisterServerSettings<TSettings>(string settingsName, Func<TSettings> getSettings) where TSettings : IBreakoutSerializable, new() { BreakoutSettingsSyncRegistry.RegisterServerSettings(settingsName, getSettings); } public static void BroadcastNow() { BreakoutSettingsSyncRegistry.BroadcastServerSettings(); } } public static class BreakoutSide { public static bool IsServer { get { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } public static bool IsClient => !Application.isBatchMode; public static bool IsDedicatedServer { get { if (Application.isBatchMode) { return IsServer; } return false; } } public static bool IsListenServer { get { if (IsServer) { return !Application.isBatchMode; } return false; } } public static bool IsInWorld { get { if ((Object)(object)ZNet.instance != (Object)null) { return ZRoutedRpc.instance != null; } return false; } } } public sealed class BreakoutSubscription : IDisposable { private readonly Action unsubscribe; private bool disposed; public bool IsDisposed => disposed; internal BreakoutSubscription(Action unsubscribe) { this.unsubscribe = unsubscribe ?? ((Action)delegate { }); } public void Dispose() { if (!disposed) { disposed = true; unsubscribe(); } } } public interface IBreakoutEvent { } public interface IBreakoutModule { void Initialize(BreakoutModuleContext context); } public interface IBreakoutSerializable { void Write(ZPackage package); void Read(ZPackage package); } internal static class BreakoutCoreHookRegistry { public const string PeerJoinedHookName = "breakoutnet.core.peer.joined"; public const string PeerLeftHookName = "breakoutnet.core.peer.left"; private const string CoreModGuid = "com.breakoutmods.valheim.breakoutnet"; public static BreakoutSubscription Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class, IBreakoutEvent { return BreakoutEventRegistry.SubscribeNamed("com.breakoutmods.valheim.breakoutnet", typeof(TEvent).FullName, handler); } public static BreakoutSubscription Subscribe<TEvent>(string hookName, Action<TEvent> handler) where TEvent : class, IBreakoutEvent { return BreakoutEventRegistry.SubscribeNamed("com.breakoutmods.valheim.breakoutnet", hookName, handler); } public static void PublishNetworkReady() { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", typeof(BreakoutNetworkReadyEvent).FullName, new BreakoutNetworkReadyEvent(BreakoutSide.IsServer, BreakoutSide.IsClient, BreakoutSide.IsDedicatedServer, BreakoutSide.IsListenServer)); } public static void PublishWorldLeft() { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", typeof(BreakoutWorldLeftEvent).FullName, new BreakoutWorldLeftEvent()); } public static void PublishPeerJoined(BreakoutPeerChangedEvent evt) { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", "breakoutnet.core.peer.joined", evt); } public static void PublishPeerLeft(BreakoutPeerChangedEvent evt) { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", "breakoutnet.core.peer.left", evt); } public static void PublishRpcReceived(BreakoutRpcObservedEvent evt) { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", typeof(BreakoutRpcObservedEvent).FullName, evt); } public static void PublishRpcRejected(long senderPeerId, string rpcName, string reason, string category) { BreakoutEventRegistry.PublishNamed("com.breakoutmods.valheim.breakoutnet", typeof(BreakoutRpcRejectedEvent).FullName, new BreakoutRpcRejectedEvent(senderPeerId, rpcName, reason, category)); } } internal static class BreakoutEventRegistry { private interface ISubscription { void Invoke(object evt, string publisherModGuid); } private sealed class Subscription<TEvent> : ISubscription where TEvent : class, IBreakoutEvent { private readonly string subscriberModGuid; private readonly string eventName; private readonly Action<TEvent> handler; public Subscription(string subscriberModGuid, string eventName, Action<TEvent> handler) { this.subscriberModGuid = subscriberModGuid; this.eventName = eventName; this.handler = handler; } public void Invoke(object evt, string publisherModGuid) { if (!(evt is TEvent obj)) { BreakoutLog.Warning("Rejected event '{0}' for subscriber '{1}' because payload type '{2}' does not match '{3}'.", eventName, subscriberModGuid, (evt != null) ? evt.GetType().FullName : "null", typeof(TEvent).FullName); return; } try { handler(obj); } catch (Exception ex) { BreakoutLog.Error("Event '{0}' handler for subscriber '{1}' failed. Publisher='{2}', payload='{3}': {4}", eventName, subscriberModGuid, publisherModGuid, typeof(TEvent).FullName, ex); } } } private struct EventKey { private readonly string scope; private readonly string typeName; private EventKey(string scope, Type type) { this.scope = scope ?? string.Empty; typeName = type.FullName; } public static EventKey Scoped(string modGuid, Type type) { return new EventKey("scope:" + modGuid, type); } public static EventKey Named(string eventName, Type type) { return new EventKey("name:" + eventName, type); } public override int GetHashCode() { return (((scope != null) ? scope.GetHashCode() : 0) * 397) ^ ((typeName != null) ? typeName.GetHashCode() : 0); } public override bool Equals(object obj) { if (!(obj is EventKey eventKey)) { return false; } if (string.Equals(scope, eventKey.scope, StringComparison.Ordinal)) { return string.Equals(typeName, eventKey.typeName, StringComparison.Ordinal); } return false; } public override string ToString() { return scope + ":" + typeName; } } private sealed class TestEvent : IBreakoutEvent { } private static readonly Dictionary<EventKey, List<ISubscription>> Subscriptions = new Dictionary<EventKey, List<ISubscription>>(); public static BreakoutSubscription SubscribeScoped<TEvent>(string modGuid, Action<TEvent> handler) where TEvent : class, IBreakoutEvent { if (handler == null) { throw new ArgumentNullException("handler"); } EventKey key = EventKey.Scoped(modGuid, typeof(TEvent)); return AddSubscription(key, new Subscription<TEvent>(modGuid, key.ToString(), handler)); } public static BreakoutSubscription SubscribeNamed<TEvent>(string modGuid, string eventName, Action<TEvent> handler) where TEvent : class, IBreakoutEvent { if (string.IsNullOrWhiteSpace(eventName)) { throw new ArgumentException("Event name cannot be empty.", "eventName"); } if (handler == null) { throw new ArgumentNullException("handler"); } return AddSubscription(EventKey.Named(eventName, typeof(TEvent)), new Subscription<TEvent>(modGuid, eventName, handler)); } public static void PublishScoped<TEvent>(string modGuid, TEvent evt) where TEvent : class, IBreakoutEvent { Publish(EventKey.Scoped(modGuid, typeof(TEvent)), modGuid, evt); } public static void PublishNamed<TEvent>(string modGuid, string eventName, TEvent evt) where TEvent : class, IBreakoutEvent { if (string.IsNullOrWhiteSpace(eventName)) { throw new ArgumentException("Event name cannot be empty.", "eventName"); } Publish(EventKey.Named(eventName, typeof(TEvent)), modGuid, evt); } private static BreakoutSubscription AddSubscription(EventKey key, ISubscription subscription) { if (!Subscriptions.TryGetValue(key, out var value)) { value = new List<ISubscription>(); Subscriptions[key] = value; } value.Add(subscription); return new BreakoutSubscription(delegate { RemoveSubscription(key, subscription); }); } private static void RemoveSubscription(EventKey key, ISubscription subscription) { if (Subscriptions.TryGetValue(key, out var value)) { value.Remove(subscription); if (value.Count == 0) { Subscriptions.Remove(key); } } } private static void Publish<TEvent>(EventKey key, string publisherModGuid, TEvent evt) where TEvent : class, IBreakoutEvent { List<ISubscription> value; if (evt == null) { BreakoutLog.Warning("Ignored null event '{0}' from mod '{1}'.", key, publisherModGuid); } else if (Subscriptions.TryGetValue(key, out value)) { ISubscription[] array = value.ToArray(); for (int i = 0; i < array.Length; i++) { array[i].Invoke(evt, publisherModGuid); } } } internal static bool EventWithMultipleSubscribersIsDeliveredForTest() { int count = 0; BreakoutSubscription breakoutSubscription = SubscribeScoped<TestEvent>("test.mod", delegate { count++; }); BreakoutSubscription breakoutSubscription2 = SubscribeScoped<TestEvent>("test.mod", delegate { count++; }); PublishScoped("test.mod", new TestEvent()); breakoutSubscription.Dispose(); breakoutSubscription2.Dispose(); return count == 2; } internal static bool DisposedSubscriptionStopsDeliveryForTest() { int count = 0; SubscribeScoped<TestEvent>("test.mod", delegate { count++; }).Dispose(); PublishScoped("test.mod", new TestEvent()); return count == 0; } internal static bool SubscriberExceptionDoesNotStopDispatchForTest() { int count = 0; BreakoutSubscription breakoutSubscription = SubscribeScoped<TestEvent>("test.mod", delegate { throw new InvalidOperationException("test"); }); BreakoutSubscription breakoutSubscription2 = SubscribeScoped<TestEvent>("test.mod", delegate { count++; }); PublishScoped("test.mod", new TestEvent()); breakoutSubscription.Dispose(); breakoutSubscription2.Dispose(); return count == 1; } } internal static class BreakoutLog { private static readonly BreakoutRateLimiter MalformedLogLimiter = new BreakoutRateLimiter(4f, 0.5f); internal static ManualLogSource Logger { get; set; } public static void Info(string message, params object[] args) { ManualLogSource logger = Logger; if (logger != null) { logger.LogInfo((object)Format(message, args)); } } public static void Warning(string message, params object[] args) { ManualLogSource logger = Logger; if (logger != null) { logger.LogWarning((object)Format(message, args)); } } public static void Error(string message, params object[] args) { ManualLogSource logger = Logger; if (logger != null) { logger.LogError((object)Format(message, args)); } } public static void Malformed(long peerId, string message, params object[] args) { string key = "malformed:" + peerId; if (MalformedLogLimiter.Allow(key)) { Warning(message, args); } } private static string Format(string message, object[] args) { if (args != null && args.Length != 0) { return string.Format(message, args); } return message; } } internal sealed class BreakoutModAppOwner : MonoBehaviour { private readonly List<BreakoutModApp> apps = new List<BreakoutModApp>(); public void Add(BreakoutModApp app) { if (app != null && !apps.Contains(app)) { apps.Add(app); } } private void OnDestroy() { BreakoutModApp[] array = apps.ToArray(); for (int i = 0; i < array.Length; i++) { array[i].Dispose(); } apps.Clear(); } } [BepInPlugin("com.breakoutmods.valheim.breakoutnet", "BreakoutNet", "0.2.1")] public sealed class BreakoutNetPlugin : BaseUnityPlugin { public const string PluginGuid = "com.breakoutmods.valheim.breakoutnet"; public const string PluginName = "BreakoutNet"; public const string PluginVersion = "0.2.1"; private GameObject runnerObject; private void Awake() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown BreakoutLog.Logger = ((BaseUnityPlugin)this).Logger; runnerObject = new GameObject("BreakoutNet"); Object.DontDestroyOnLoad((Object)(object)runnerObject); runnerObject.AddComponent<BreakoutNetRunner>(); BreakoutLog.Info("{0} {1} loaded.", "BreakoutNet", "0.2.1"); } private void OnDestroy() { if ((Object)(object)runnerObject != (Object)null) { Object.Destroy((Object)(object)runnerObject); runnerObject = null; } } } internal sealed class BreakoutNetRunner : MonoBehaviour { internal const string RoutedRpcName = "BreakoutNet.RPC"; private ZRoutedRpc registeredInstance; private float nextSettingsBroadcast; private bool broadcastedThisSession; private readonly HashSet<long> knownPeers = new HashSet<long>(); private void Update() { if (ZRoutedRpc.instance == null) { if (registeredInstance != null) { BreakoutCoreHookRegistry.PublishWorldLeft(); knownPeers.Clear(); } registeredInstance = null; broadcastedThisSession = false; return; } if (registeredInstance != ZRoutedRpc.instance) { registeredInstance = ZRoutedRpc.instance; registeredInstance.Register<ZPackage>("BreakoutNet.RPC", (Action<long, ZPackage>)OnRoutedRpc); broadcastedThisSession = false; BreakoutLog.Info("Registered routed RPC endpoint '{0}'.", "BreakoutNet.RPC"); BreakoutCoreHookRegistry.PublishNetworkReady(); } UpdatePeerHooks(); if (BreakoutSide.IsServer) { if (!broadcastedThisSession) { BreakoutSettingsSyncRegistry.BroadcastServerSettings(); broadcastedThisSession = true; nextSettingsBroadcast = Time.time + 10f; } if (Time.time >= nextSettingsBroadcast) { BreakoutSettingsSyncRegistry.BroadcastServerSettings(); nextSettingsBroadcast = Time.time + 10f; } } } private static void OnRoutedRpc(long senderPeerId, ZPackage package) { BreakoutRpcRegistry.Dispatch(senderPeerId, package); } private void UpdatePeerHooks() { if ((Object)(object)ZNet.instance == (Object)null) { return; } HashSet<long> hashSet = new HashSet<long>(); foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers()) { if (connectedPeer != null) { hashSet.Add(connectedPeer.m_uid); if (!knownPeers.Contains(connectedPeer.m_uid)) { BreakoutCoreHookRegistry.PublishPeerJoined(CreatePeerEvent(connectedPeer, isServerPeer: false)); } } } ZNetPeer val = ((!ZNet.instance.IsServer()) ? ZNet.instance.GetServerPeer() : null); if (val != null) { hashSet.Add(val.m_uid); if (!knownPeers.Contains(val.m_uid)) { BreakoutCoreHookRegistry.PublishPeerJoined(CreatePeerEvent(val, isServerPeer: true)); } } foreach (long knownPeer in knownPeers) { if (!hashSet.Contains(knownPeer)) { BreakoutCoreHookRegistry.PublishPeerLeft(new BreakoutPeerChangedEvent(knownPeer, string.Empty, isServerPeer: false, knownPeer == ZNet.GetUID())); } } knownPeers.Clear(); foreach (long item in hashSet) { knownPeers.Add(item); } } private static BreakoutPeerChangedEvent CreatePeerEvent(ZNetPeer peer, bool isServerPeer) { string empty = string.Empty; try { empty = peer.m_playerName; } catch { empty = string.Empty; } return new BreakoutPeerChangedEvent(peer.m_uid, empty, isServerPeer, peer.m_uid == ZNet.GetUID()); } } internal sealed class BreakoutRateLimiter { private struct Bucket { public float Tokens; public float LastUpdate; } private readonly Dictionary<string, Bucket> buckets = new Dictionary<string, Bucket>(); private readonly float capacity; private readonly float refillPerSecond; public BreakoutRateLimiter(float capacity, float refillPerSecond) { this.capacity = Math.Max(1f, capacity); this.refillPerSecond = Math.Max(0f, refillPerSecond); } public bool Allow(string key) { return Allow(key, Time.realtimeSinceStartup); } internal bool Allow(string key, float now) { return Allow(key, now, capacity, refillPerSecond); } internal bool Allow(string key, float now, float bucketCapacity, float bucketRefillPerSecond) { if (string.IsNullOrWhiteSpace(key)) { key = "unknown"; } bucketCapacity = Math.Max(1f, bucketCapacity); bucketRefillPerSecond = Math.Max(0f, bucketRefillPerSecond); if (!buckets.TryGetValue(key, out var value)) { Bucket bucket = default(Bucket); bucket.Tokens = bucketCapacity; bucket.LastUpdate = now; value = bucket; } float num = Math.Max(0f, now - value.LastUpdate); value.Tokens = Math.Min(bucketCapacity, value.Tokens + num * bucketRefillPerSecond); value.LastUpdate = now; if (value.Tokens < 1f) { buckets[key] = value; return false; } value.Tokens -= 1f; buckets[key] = value; return true; } } internal sealed class BreakoutRpcEnvelope { public const int CurrentProtocolVersion = 1; public int ProtocolVersion { get; private set; } public string RpcName { get; private set; } public string SenderModGuid { get; private set; } public string MessageTypeName { get; private set; } public int Sequence { get; private set; } public static void WriteHeader(ZPackage package, string rpcName, string senderModGuid, string messageTypeName, int sequence) { package.Write(1); package.Write(rpcName ?? string.Empty); package.Write(string.IsNullOrWhiteSpace(senderModGuid) ? "com.breakoutmods.valheim.breakoutnet" : senderModGuid); package.Write(messageTypeName ?? string.Empty); package.Write(sequence); } public static bool TryReadHeader(ZPackage package, out BreakoutRpcEnvelope envelope, out string reason) { envelope = null; reason = null; if (package == null) { reason = "Package is null."; return false; } try { envelope = new BreakoutRpcEnvelope { ProtocolVersion = package.ReadInt(), RpcName = package.ReadString(), SenderModGuid = package.ReadString(), MessageTypeName = package.ReadString(), Sequence = package.ReadInt() }; } catch (Exception ex) { reason = "Malformed BreakoutNet envelope: " + ex.Message; return false; } if (envelope.ProtocolVersion != 1) { reason = "Unsupported protocol version " + envelope.ProtocolVersion + "."; return false; } if (string.IsNullOrWhiteSpace(envelope.RpcName)) { reason = "RPC name is empty."; return false; } if (string.IsNullOrWhiteSpace(envelope.MessageTypeName)) { reason = "Message type name is empty."; return false; } return true; } } internal static class BreakoutRpcRegistry { private interface IHandler { string MessageTypeName { get; } BreakoutRpcRateLimit RateLimit { get; } void Invoke(BreakoutRpcContext context, ZPackage package); } private sealed class Handler<TMessage> : IHandler where TMessage : IBreakoutSerializable, new() { private readonly BreakoutRpcHandler<TMessage> handler; private readonly BreakoutRpcRateLimit rateLimit; public string MessageTypeName => typeof(TMessage).FullName; public BreakoutRpcRateLimit RateLimit => rateLimit; public Handler(BreakoutRpcHandler<TMessage> handler, BreakoutRpcRateLimit rateLimit) { this.handler = handler ?? throw new ArgumentNullException("handler"); this.rateLimit = rateLimit ?? BreakoutRpcRateLimit.Default; } public void Invoke(BreakoutRpcContext context, ZPackage package) { try { TMessage message = new TMessage(); message.Read(package); handler(context, message); } catch (Exception ex) { BreakoutLog.Error("RPC '{0}' handler failed for peer {1}: {2}", context.RpcName, context.SenderPeerId, ex); } } } private static readonly Dictionary<string, IHandler> ServerHandlers = new Dictionary<string, IHandler>(); private static readonly Dictionary<string, IHandler> ClientHandlers = new Dictionary<string, IHandler>(); private static readonly BreakoutRateLimiter InboundClientLimiter = new BreakoutRateLimiter(12f, 8f); private static int sequence; public static void RegisterServer<TMessage>(string rpcName, BreakoutRpcHandler<TMessage> handler, BreakoutRpcRateLimit rateLimit) where TMessage : IBreakoutSerializable, new() { Register(ServerHandlers, rpcName, new Handler<TMessage>(handler, rateLimit)); BreakoutLog.Info("Registered server RPC '{0}' for {1} with {2}.", rpcName, typeof(TMessage).FullName, DescribeRateLimit(rateLimit)); } public static void RegisterClient<TMessage>(string rpcName, BreakoutRpcHandler<TMessage> handler) where TMessage : IBreakoutSerializable, new() { Register(ClientHandlers, rpcName, new Handler<TMessage>(handler, BreakoutRpcRateLimit.Unlimited)); BreakoutLog.Info("Registered client RPC '{0}' for {1}.", rpcName, typeof(TMessage).FullName); } public static bool SendToServer<TMessage>(string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { if (!CanSend(rpcName, message)) { return false; } if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return DispatchLocalAsServer(rpcName, message, senderModGuid); } ZNetPeer serverPeer = BreakoutPeers.ServerPeer; if (serverPeer == null) { BreakoutLog.Warning("Cannot send RPC '{0}' to server because no server peer is available.", rpcName); return false; } SendPackage(serverPeer.m_uid, rpcName, message, senderModGuid); return true; } public static bool SendToPeer<TMessage>(long peerId, string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { if (!BreakoutSide.IsServer) { BreakoutLog.Warning("Cannot send server RPC '{0}' to peer {1}; this side is not server-authoritative.", rpcName, peerId); return false; } if (!CanSend(rpcName, message)) { return false; } if (BreakoutSide.IsListenServer && peerId == ZNet.GetUID()) { return DispatchLocalAsClient(rpcName, message, senderModGuid); } SendPackage(peerId, rpcName, message, senderModGuid); return true; } public static bool Broadcast<TMessage>(string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { if (!BreakoutSide.IsServer) { BreakoutLog.Warning("Cannot broadcast RPC '{0}'; this side is not server-authoritative.", rpcName); return false; } if (!CanSend(rpcName, message)) { return false; } foreach (ZNetPeer connectedPeer in BreakoutPeers.ConnectedPeers) { if (connectedPeer != null) { SendPackage(connectedPeer.m_uid, rpcName, message, senderModGuid); } } if (BreakoutSide.IsListenServer) { DispatchLocalAsClient(rpcName, message, senderModGuid); } return true; } public static int BroadcastExcept<TMessage>(long senderPeerId, string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { if (!BreakoutSide.IsServer || !CanSend(rpcName, message)) { return 0; } int num = 0; foreach (ZNetPeer connectedPeer in BreakoutPeers.ConnectedPeers) { if (connectedPeer != null && connectedPeer.m_uid != senderPeerId) { SendPackage(connectedPeer.m_uid, rpcName, message, senderModGuid); num++; } } if (BreakoutSide.IsListenServer && senderPeerId != ZNet.GetUID() && DispatchLocalAsClient(rpcName, message, senderModGuid)) { num++; } return num; } public static int BroadcastNear<TMessage>(Vector3 position, float radius, string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) if (!BreakoutSide.IsServer || !CanSend(rpcName, message)) { return 0; } float num = Mathf.Max(0f, radius) * Mathf.Max(0f, radius); int num2 = 0; Vector3 val; foreach (ZNetPeer connectedPeer in BreakoutPeers.ConnectedPeers) { if (connectedPeer != null) { val = connectedPeer.GetRefPos() - position; if (!(((Vector3)(ref val)).sqrMagnitude > num)) { SendPackage(connectedPeer.m_uid, rpcName, message, senderModGuid); num2++; } } } if (BreakoutSide.IsListenServer && (Object)(object)Player.m_localPlayer != (Object)null) { val = ((Component)Player.m_localPlayer).transform.position - position; if (((Vector3)(ref val)).sqrMagnitude <= num && DispatchLocalAsClient(rpcName, message, senderModGuid)) { num2++; } } return num2; } public static void Dispatch(long senderPeerId, ZPackage package) { bool isServer = BreakoutSide.IsServer; bool isFromServer = IsSenderServer(senderPeerId); DispatchToHandlers(senderPeerId, package, isServer, isFromServer); } internal static bool TryCreateDispatchPlanForTest(bool isServerSide, bool isFromServer, string rpcName, out string reason) { reason = null; if (string.IsNullOrWhiteSpace(rpcName)) { reason = "RPC name is empty."; return false; } if (!isServerSide && !isFromServer) { reason = "Client-side RPC must come from server."; return false; } return true; } private static void DispatchToHandlers(long senderPeerId, ZPackage package, bool isServerSide, bool isFromServer) { if (!BreakoutRpcEnvelope.TryReadHeader(package, out var envelope, out var reason)) { BreakoutLog.Malformed(senderPeerId, "Rejected malformed BreakoutNet packet from peer {0}: {1}", senderPeerId, reason); BreakoutCoreHookRegistry.PublishRpcRejected(senderPeerId, string.Empty, reason, "malformed"); return; } BreakoutCoreHookRegistry.PublishRpcReceived(new BreakoutRpcObservedEvent(senderPeerId, isFromServer, isServerSide, envelope.RpcName, envelope.SenderModGuid, envelope.MessageTypeName, envelope.Sequence)); if (!(isServerSide ? ServerHandlers : ClientHandlers).TryGetValue(envelope.RpcName, out var value)) { BreakoutLog.Malformed(senderPeerId, "Rejected unregistered RPC '{0}' from peer {1}.", envelope.RpcName, senderPeerId); BreakoutCoreHookRegistry.PublishRpcRejected(senderPeerId, envelope.RpcName, "Unregistered RPC.", "unregistered"); return; } if (!isServerSide && !isFromServer) { BreakoutLog.Malformed(senderPeerId, "Rejected client-side RPC '{0}' from non-server peer {1}.", envelope.RpcName, senderPeerId); BreakoutCoreHookRegistry.PublishRpcRejected(senderPeerId, envelope.RpcName, "Client-side RPC came from a non-server peer.", "unauthorized"); return; } if (!string.Equals(value.MessageTypeName, envelope.MessageTypeName, StringComparison.Ordinal)) { BreakoutLog.Malformed(senderPeerId, "Rejected RPC '{0}' from peer {1}; expected message type '{2}' but received '{3}'.", envelope.RpcName, senderPeerId, value.MessageTypeName, envelope.MessageTypeName); BreakoutCoreHookRegistry.PublishRpcRejected(senderPeerId, envelope.RpcName, "Message type mismatch.", "type-mismatch"); return; } if (isServerSide && !isFromServer) { BreakoutRpcRateLimit breakoutRpcRateLimit = value.RateLimit ?? BreakoutRpcRateLimit.Default; if (breakoutRpcRateLimit.Enabled) { string key = senderPeerId + ":" + envelope.RpcName; if (!InboundClientLimiter.Allow(key, Time.realtimeSinceStartup, breakoutRpcRateLimit.Capacity, breakoutRpcRateLimit.RefillPerSecond)) { BreakoutLog.Malformed(senderPeerId, "Rate-limited inbound RPC '{0}' from peer {1}; capacity={2:0.##}, refill={3:0.##}/s.", envelope.RpcName, senderPeerId, breakoutRpcRateLimit.Capacity, breakoutRpcRateLimit.RefillPerSecond); BreakoutCoreHookRegistry.PublishRpcRejected(senderPeerId, envelope.RpcName, "Rate-limited inbound RPC.", "rate-limit"); return; } } } BreakoutRpcContext context = new BreakoutRpcContext(senderPeerId, isFromServer, isServerSide, envelope.RpcName, envelope.SenderModGuid, envelope.Sequence); value.Invoke(context, package); } private static void Register(Dictionary<string, IHandler> handlers, string rpcName, IHandler handler) { if (string.IsNullOrWhiteSpace(rpcName)) { throw new ArgumentException("RPC name cannot be empty.", "rpcName"); } if (handler == null) { throw new ArgumentNullException("handler"); } handlers[rpcName] = handler; } private static bool CanSend<TMessage>(string rpcName, TMessage message) where TMessage : IBreakoutSerializable, new() { if (ZRoutedRpc.instance == null) { BreakoutLog.Warning("Cannot send RPC '{0}' because ZRoutedRpc is not ready.", rpcName); return false; } if (string.IsNullOrWhiteSpace(rpcName)) { BreakoutLog.Warning("Cannot send RPC with an empty name."); return false; } if (message == null) { BreakoutLog.Warning("Cannot send RPC '{0}' because message is null.", rpcName); return false; } return true; } private static void SendPackage<TMessage>(long targetPeerId, string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { ZPackage val = CreatePackage(rpcName, message, senderModGuid); ZRoutedRpc.instance.InvokeRoutedRPC(targetPeerId, "BreakoutNet.RPC", new object[1] { val }); } private static bool DispatchLocalAsServer<TMessage>(string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { ZPackage package = CreatePackage(rpcName, message, senderModGuid); Dispatch(((Object)(object)ZNet.instance != (Object)null) ? ZNet.GetUID() : 0, package); return true; } private static bool DispatchLocalAsClient<TMessage>(string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { ZPackage package = CreatePackage(rpcName, message, senderModGuid); DispatchToHandlers(((Object)(object)ZNet.instance != (Object)null) ? ZNet.GetUID() : 0, package, isServerSide: false, isFromServer: true); return true; } private static ZPackage CreatePackage<TMessage>(string rpcName, TMessage message, string senderModGuid) where TMessage : IBreakoutSerializable, new() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); int num = Interlocked.Increment(ref sequence); BreakoutRpcEnvelope.WriteHeader(val, rpcName, senderModGuid, typeof(TMessage).FullName, num); message.Write(val); val.SetPos(0); return val; } private static string DescribeRateLimit(BreakoutRpcRateLimit rateLimit) { rateLimit = rateLimit ?? BreakoutRpcRateLimit.Default; if (!rateLimit.Enabled) { return "no inbound client rate limit"; } return $"inbound client rate limit capacity={rateLimit.Capacity:0.##}, refill={rateLimit.RefillPerSecond:0.##}/s"; } private static bool IsSenderServer(long senderPeerId) { if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (ZNet.instance.IsServer()) { return senderPeerId == ZNet.GetUID(); } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null) { return serverPeer.m_uid == senderPeerId; } return false; } } internal static class BreakoutSettingsSyncRegistry { private interface ISettingsProvider { void Broadcast(); } private sealed class SettingsProvider<TSettings> : ISettingsProvider where TSettings : IBreakoutSerializable, new() { private readonly string settingsName; private readonly Func<TSettings> getSettings; public SettingsProvider(string settingsName, Func<TSettings> getSettings) { this.settingsName = settingsName; this.getSettings = getSettings; } public void Broadcast() { try { TSettings val = getSettings(); if (val == null) { BreakoutLog.Warning("Settings sync '{0}' returned null and was not broadcast.", settingsName); } else { BreakoutRpc.Server.Broadcast(BuildRpcName(settingsName), val, "com.breakoutmods.valheim.breakoutnet"); } } catch (Exception ex) { BreakoutLog.Error("Settings sync '{0}' failed: {1}", settingsName, ex); } } } public const float BroadcastIntervalSeconds = 10f; private const string RpcPrefix = "breakoutnet.settings."; private static readonly Dictionary<string, ISettingsProvider> ServerProviders = new Dictionary<string, ISettingsProvider>(); public static void RegisterServerSettings<TSettings>(string settingsName, Func<TSettings> getSettings) where TSettings : IBreakoutSerializable, new() { if (string.IsNullOrWhiteSpace(settingsName)) { throw new ArgumentException("Settings name cannot be empty.", "settingsName"); } if (getSettings == null) { throw new ArgumentNullException("getSettings"); } ServerProviders[settingsName] = new SettingsProvider<TSettings>(settingsName, getSettings); BreakoutLog.Info("Registered server settings sync '{0}'.", settingsName); } public static void RegisterClientSettings<TSettings>(string settingsName, Action<TSettings> applySettings) where TSettings : IBreakoutSerializable, new() { if (string.IsNullOrWhiteSpace(settingsName)) { throw new ArgumentException("Settings name cannot be empty.", "settingsName"); } if (applySettings == null) { throw new ArgumentNullException("applySettings"); } BreakoutRpc.Client.Register(BuildRpcName(settingsName), delegate(BreakoutRpcContext context, TSettings settings) { if (!context.IsFromServer) { context.Reject("Settings sync packets are only accepted from the server."); } else { applySettings(settings); } }); BreakoutLog.Info("Registered client settings sync '{0}'.", settingsName); } public static void BroadcastServerSettings() { if (!BreakoutSide.IsServer || ZRoutedRpc.instance == null) { return; } foreach (ISettingsProvider value in ServerProviders.Values) { value.Broadcast(); } } private static string BuildRpcName(string settingsName) { return "breakoutnet.settings." + settingsName; } } internal static class BreakoutValidationHarness { private sealed class EmptyMessage : IBreakoutSerializable { public void Write(ZPackage package) { } public void Read(ZPackage package) { } } public static bool UnknownProtocolVersionFails() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003b: 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) //IL_0052: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(999); val.Write("test.rpc"); val.Write("com.breakoutmods.valheim.breakoutnet"); val.Write(typeof(EmptyMessage).FullName); val.Write(1); val.SetPos(0); if (!BreakoutRpcEnvelope.TryReadHeader(val, out var _, out var reason)) { return reason.Contains("Unsupported protocol version"); } return false; } public static bool MalformedPackageFails() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.SetPos(0); BreakoutRpcEnvelope envelope; string reason; return !BreakoutRpcEnvelope.TryReadHeader(val, out envelope, out reason); } public static bool ClientSideNonServerMessageFails() { string reason; return !BreakoutRpcRegistry.TryCreateDispatchPlanForTest(isServerSide: false, isFromServer: false, "test.rpc", out reason); } public static bool RateLimiterDropsExcessiveMessages() { BreakoutRateLimiter breakoutRateLimiter = new BreakoutRateLimiter(1f, 0f); if (breakoutRateLimiter.Allow("peer:rpc", 0f)) { return !breakoutRateLimiter.Allow("peer:rpc", 0.01f); } return false; } public static bool RateLimiterUsesCustomPolicy() { BreakoutRateLimiter breakoutRateLimiter = new BreakoutRateLimiter(1f, 0f); BreakoutRpcRateLimit breakoutRpcRateLimit = BreakoutRpcRateLimit.ForMessagesPerSecond(60f, 3f); if (breakoutRateLimiter.Allow("peer:voice", 0f, breakoutRpcRateLimit.Capacity, breakoutRpcRateLimit.RefillPerSecond)) { return breakoutRateLimiter.Allow("peer:voice", 0.01f, breakoutRpcRateLimit.Capacity, breakoutRpcRateLimit.RefillPerSecond); } return false; } public static bool EventWithMultipleSubscribersIsDelivered() { return BreakoutEventRegistry.EventWithMultipleSubscribersIsDeliveredForTest(); } public static bool DisposedSubscriptionStopsDelivery() { return BreakoutEventRegistry.DisposedSubscriptionStopsDeliveryForTest(); } public static bool SubscriberExceptionDoesNotStopDispatch() { return BreakoutEventRegistry.SubscriberExceptionDoesNotStopDispatchForTest(); } } }