using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Costura;
using HarmonyLib;
using Steamworks;
using UnityEngine;
using ZstdSharp;
[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("CW_Jesse")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("CW_Jesse 2023")]
[assembly: AssemblyFileVersion("2.3.2")]
[assembly: AssemblyInformationalVersion("2.3.2")]
[assembly: AssemblyProduct("CW_Jesse.BetterNetworking")]
[assembly: AssemblyTitle("CW_Jesse.BetterNetworking")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/CW-Jesse/valheim-betternetworking")]
[assembly: AssemblyVersion("2.3.2.0")]
internal class <Module>
{
static <Module>()
{
AssemblyLoader.Attach();
}
}
namespace CW_Jesse.BetterNetworking
{
[BepInPlugin("CW_Jesse.BetterNetworking", "Better Networking", "2.3.2")]
[BepInIncompatibility("org.bepinex.plugins.network")]
[BepInIncompatibility("be.sebastienvercammen.valheim.netcompression")]
[BepInIncompatibility("com.github.dalayeth.Networkfix")]
[BepInIncompatibility("Steel.ValheimMod")]
public class BetterNetworking : BaseUnityPlugin
{
private readonly Harmony harmony = new Harmony("CW_Jesse.BetterNetworking");
public static ConfigEntry<BN_Logger.Options_Logger_LogLevel> configLogMessages;
public static ConfigEntry<BN_Patch_ForceCrossplay.Options_ForceCrossplay> configForceCrossplay;
public static ConfigEntry<int> configPlayerLimit;
public static ConfigEntry<BN_Patch_Compression.Options_NetworkCompression> configCompressionEnabled;
public static ConfigEntry<BN_Patch_UpdateRate.Options_NetworkUpdateRates> configNetworkUpdateRate;
public static ConfigEntry<BN_Patch_SendRate.Options_NetworkSendRateMin> configNetworkSendRateMin;
public static ConfigEntry<BN_Patch_SendRate.Options_NetworkSendRateMax> configNetworkSendRateMax;
public static ConfigEntry<BN_Patch_QueueSize.Options_NetworkQueueSize> configNetworkQueueSize;
private void Awake()
{
BN_Logger.Init(((BaseUnityPlugin)this).Logger, ((BaseUnityPlugin)this).Config);
BN_Patch_Compression.InitCompressor();
BN_Patch_Compression.InitConfig(((BaseUnityPlugin)this).Config);
BN_Patch_UpdateRate.InitConfig(((BaseUnityPlugin)this).Config);
BN_Patch_SendRate.InitConfig(((BaseUnityPlugin)this).Config);
BN_Patch_QueueSize.InitConfig(((BaseUnityPlugin)this).Config);
BN_Patch_DedicatedServer.InitConfig(((BaseUnityPlugin)this).Config);
harmony.PatchAll();
}
private void OnDestroy()
{
harmony.UnpatchSelf();
}
}
[HarmonyPatch]
public class BN_Patch_DedicatedServer
{
private static ConfigFile config;
public static void InitConfig(ConfigFile configFile)
{
config = configFile;
}
[HarmonyPatch(typeof(ZNet), "IsDedicated")]
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> DedicatedServerInit(IEnumerable<CodeInstruction> instructions)
{
bool isDedicated = false;
foreach (CodeInstruction instruction in instructions)
{
if (instruction.opcode == OpCodes.Ldc_I4_1)
{
isDedicated = true;
BN_Patch_ForceCrossplay.InitConfig(config);
BN_Patch_ChangePlayerLimit.InitConfig(config);
}
}
BN_Utils.isDedicated = isDedicated;
return instructions;
}
}
public class BN_Logger
{
public enum Options_Logger_LogLevel
{
[Description("Errors/Warnings")]
warning,
[Description("Errors/Warnings/Messages <b>[default]</b>")]
message,
[Description("Errors/Warnings/Messages/Info")]
info
}
private static ManualLogSource logger;
public static void Init(ManualLogSource logger, ConfigFile config)
{
BN_Logger.logger = logger;
BetterNetworking.configLogMessages = config.Bind<Options_Logger_LogLevel>("Logging", "Log Level", Options_Logger_LogLevel.message, "Better Network's verbosity in console/logs.");
}
public static void LogError(object data)
{
logger.LogError(data);
}
public static void LogWarning(object data)
{
logger.LogWarning(data);
}
public static void LogMessage(object data)
{
if (BetterNetworking.configLogMessages.Value >= Options_Logger_LogLevel.message)
{
logger.LogMessage(data);
}
}
public static void LogInfo(object data)
{
if (BetterNetworking.configLogMessages.Value >= Options_Logger_LogLevel.info)
{
logger.LogInfo(data);
}
}
}
internal class BN_Utils
{
public static bool isDedicated;
public static ZNetPeer GetPeer(ZRpc rpc)
{
foreach (ZNetPeer peer in ZNet.instance.GetPeers())
{
if (peer.m_rpc == rpc)
{
return peer;
}
}
return null;
}
public static ZNetPeer GetPeer(ISocket socket)
{
foreach (ZNetPeer peer in ZNet.instance.GetPeers())
{
if (peer.m_socket == socket)
{
return peer;
}
}
return null;
}
public static string GetPeerName(ZNetPeer peer)
{
if (peer == null)
{
return "[null]";
}
if (peer.m_server)
{
return "[server]";
}
return peer.m_playerName + "[" + peer.m_socket.GetHostName() + "]";
}
public static string GetPeerName(ISocket socket)
{
return GetPeerName(GetPeer(socket));
}
}
[HarmonyPatch]
public class BN_Patch_ChangePlayerLimit
{
public static void InitConfig(ConfigFile config)
{
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Expected O, but got Unknown
BetterNetworking.configPlayerLimit = config.Bind<int>("Dedicated Server", "Player Limit", 10, new ConfigDescription("Requires restart. Changes player limit for dedicated servers.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 127), Array.Empty<object>()));
}
[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
[HarmonyTranspiler]
public static IEnumerable<CodeInstruction> SetPlayerLimit(IEnumerable<CodeInstruction> instructions)
{
foreach (CodeInstruction instruction in instructions)
{
if (BN_Utils.isDedicated && CodeInstructionExtensions.Is(instruction, OpCodes.Ldc_I4_S, (object)(sbyte)10))
{
yield return new CodeInstruction(OpCodes.Ldc_I4_S, (object)(sbyte)BetterNetworking.configPlayerLimit.Value);
}
else
{
yield return instruction;
}
}
}
}
[HarmonyPatch]
public class BN_Patch_ForceCrossplay
{
public enum Options_ForceCrossplay
{
[Description("Vanilla behaviour <b>[default]</b>")]
vanilla,
[Description("Crossplay ENABLED")]
playfab,
[Description("Crossplay DISABLED")]
steamworks
}
public static void InitConfig(ConfigFile config)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
BetterNetworking.configForceCrossplay = config.Bind<Options_ForceCrossplay>("Dedicated Server", "Force Crossplay", Options_ForceCrossplay.vanilla, new ConfigDescription("Requires restart.\nplayfab (crossplay enabled): Forces dedicated servers to use new PlayFab networking stack.\nsteamworks (crossplay disabled): Forces dedicated servers to use Steamworks network stack.\nvanilla: Listen for -crossplay flag.", (AcceptableValueBase)null, Array.Empty<object>()));
}
[HarmonyPatch(typeof(FejdStartup), "ParseServerArguments")]
[HarmonyPostfix]
private static void ForceCrossplay()
{
//IL_0015: 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)
if (BN_Utils.isDedicated)
{
if (BetterNetworking.configForceCrossplay.Value == Options_ForceCrossplay.playfab)
{
ZNet.m_onlineBackend = (OnlineBackendType)1;
ZPlayFabMatchmaking.LookupPublicIP();
}
if (BetterNetworking.configForceCrossplay.Value == Options_ForceCrossplay.steamworks)
{
ZNet.m_onlineBackend = (OnlineBackendType)0;
}
}
}
}
[HarmonyPatch]
public class BN_Patch_NewConnectionBuffer
{
[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
private class StartBufferingOnNewConnection
{
private static void Postfix(ZNet __instance, ZNetPeer peer)
{
if (!__instance.IsServer())
{
peer.m_rpc.Register<ZPackage>("ZDOData", (Action<ZRpc, ZPackage>)delegate(ZRpc nullPeer, ZPackage package)
{
packageBuffer.Add(package);
});
}
}
}
[HarmonyPatch(typeof(ZDOMan), "AddPeer")]
private class SendBufferOnAddPeer
{
private static void Postfix(ZDOMan __instance, ZNetPeer netPeer)
{
if (packageBuffer.Count <= 0)
{
return;
}
BN_Logger.LogWarning($"Connection buffer: Sending {packageBuffer.Count} buffered packages; Valheim or a mod is trying to send data too early");
foreach (ZPackage item in packageBuffer)
{
AccessTools.Method(typeof(ZDOMan), "RPC_ZDOData", (Type[])null, (Type[])null).Invoke(__instance, new object[2] { netPeer.m_rpc, item });
}
packageBuffer.Clear();
}
}
[HarmonyPatch(typeof(ZNet), "Shutdown")]
private class ClearBufferOnShutdown
{
private static void Postfix()
{
packageBuffer.Clear();
}
}
private static readonly List<ZPackage> packageBuffer = new List<ZPackage>();
}
[HarmonyPatch]
public class BN_Patch_QueueSize
{
public enum Options_NetworkQueueSize
{
[Description("80 KB")]
_80KB,
[Description("64 KB")]
_64KB,
[Description("48 KB")]
_48KB,
[Description("32 KB <b>[default]</b>")]
_32KB,
[Description("[Valheim default]")]
_vanilla
}
private const int DEFAULT_QUEUE_SIZE = 10240;
private const int DEFAULT_MINIMUM_QUEUE_SIZE = 2048;
public static void InitConfig(ConfigFile config)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
BetterNetworking.configNetworkQueueSize = config.Bind<Options_NetworkQueueSize>("Networking", "Queue Size", Options_NetworkQueueSize._32KB, new ConfigDescription("The better your upload speed, the higher you can set this.\nHigher options aren't available as they can cause errors in Steam.\nWith compression and 100% update rate, 32 KB spikes upload speeds to 256 KB/s. (32KB*0.4*20/s)---\nIf others experience lag/desync for things <i>around</i> you, increase your queue size.\nIf your <i>character</i> is lagging for others, decrease your update rate and/or queue size.", (AcceptableValueBase)null, Array.Empty<object>()));
}
[HarmonyPatch(typeof(ZSteamSocket), "GetSendQueueSize")]
[HarmonyPostfix]
private static void Steamworks_GetSendQueueSize(ref int __result)
{
switch (BetterNetworking.configNetworkQueueSize.Value)
{
case Options_NetworkQueueSize._80KB:
__result -= 71680;
break;
case Options_NetworkQueueSize._64KB:
__result -= 55296;
break;
case Options_NetworkQueueSize._48KB:
__result -= 38912;
break;
case Options_NetworkQueueSize._32KB:
__result -= 22528;
break;
}
}
[HarmonyPatch(typeof(ZPlayFabSocket), "GetSendQueueSize")]
[HarmonyPrefix]
private static bool PlayFab_GetSendQueueSize(ref int __result, ref InFlightQueue ___m_inFlightQueue)
{
switch (BetterNetworking.configNetworkQueueSize.Value)
{
case Options_NetworkQueueSize._80KB:
__result = (int)(___m_inFlightQueue.Bytes - 71680);
return false;
case Options_NetworkQueueSize._64KB:
__result = (int)(___m_inFlightQueue.Bytes - 55296);
return false;
case Options_NetworkQueueSize._48KB:
__result = (int)(___m_inFlightQueue.Bytes - 38912);
return false;
case Options_NetworkQueueSize._32KB:
__result = (int)(___m_inFlightQueue.Bytes - 22528);
return false;
default:
return true;
}
}
}
[HarmonyPatch]
public class BN_Patch_SendRate
{
public enum Options_NetworkSendRateMin
{
[Description("1024 KB/s | 8 Mbit/s")]
_1024KB,
[Description("768 KB/s | 6 Mbit/s")]
_768KB,
[Description("512 KB/s | 4 Mbit/s")]
_512KB,
[Description("256 KB/s | 2 Mbit/s <b>[default]</b>")]
_256KB,
[Description("150 KB/s | 1.2 Mbit/s [Valheim default]")]
_150KB
}
public enum Options_NetworkSendRateMax
{
[Description("1024 KB/s | 8 Mbit/s <b>[default]</b>")]
_1024KB,
[Description("768 KB/s | 6 Mbit/s")]
_768KB,
[Description("512 KB/s | 4 Mbit/s")]
_512KB,
[Description("256 KB/s | 2 Mbit/s")]
_256KB,
[Description("150 KB/s | 1.2 Mbit/s [Valheim default]")]
_150KB
}
[HarmonyPatch(typeof(SteamNetworkingUtils))]
[HarmonyPatch(typeof(SteamGameServerNetworkingUtils))]
private class NetworkSendRate_Patch
{
public static int SendRateMin => BetterNetworking.configNetworkSendRateMin.Value switch
{
Options_NetworkSendRateMin._1024KB => 1048576,
Options_NetworkSendRateMin._768KB => 786432,
Options_NetworkSendRateMin._512KB => 524288,
Options_NetworkSendRateMin._256KB => 262144,
_ => 153600,
};
public static int SendRateMax => BetterNetworking.configNetworkSendRateMax.Value switch
{
Options_NetworkSendRateMax._1024KB => 1048576,
Options_NetworkSendRateMax._768KB => 786432,
Options_NetworkSendRateMax._512KB => 524288,
Options_NetworkSendRateMax._256KB => 262144,
_ => 153600,
};
public static void SetSendRateMinFromConfig()
{
SetSteamNetworkConfig((ESteamNetworkingConfigValue)10, SendRateMin);
}
public static void SetSendRateMaxFromConfig()
{
SetSteamNetworkConfig((ESteamNetworkingConfigValue)11, SendRateMax);
}
public static void SetSendBufferSize()
{
SetSteamNetworkConfig((ESteamNetworkingConfigValue)9, 524288);
}
private static int GetSteamNetworkConfig(ESteamNetworkingConfigValue valueType)
{
//IL_0078: Unknown result type (might be due to invalid IL or missing references)
//IL_0012: 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_006a: 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)
//IL_0050: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)ZNet.instance == (Object)null)
{
BN_Logger.LogInfo($"Steamworks: Unable to get net config while disconnected: {valueType}");
return -1;
}
ulong num = 4uL;
byte[] value = new byte[num];
GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned);
try
{
ESteamNetworkingConfigDataType val = default(ESteamNetworkingConfigDataType);
if (BN_Utils.isDedicated)
{
SteamGameServerNetworkingUtils.GetConfigValue(valueType, (ESteamNetworkingConfigScope)1, IntPtr.Zero, ref val, gCHandle.AddrOfPinnedObject(), ref num);
}
else
{
SteamNetworkingUtils.GetConfigValue(valueType, (ESteamNetworkingConfigScope)1, IntPtr.Zero, ref val, gCHandle.AddrOfPinnedObject(), ref num);
}
}
catch
{
BN_Logger.LogError($"Steamworks: Unable to get net config: {valueType}");
}
gCHandle.Free();
return BitConverter.ToInt32(value, 0);
}
private static void SetSteamNetworkConfig(ESteamNetworkingConfigValue valueType, int value)
{
//IL_0072: Unknown result type (might be due to invalid IL or missing references)
//IL_0023: Unknown result type (might be due to invalid IL or missing references)
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
//IL_0098: Unknown result type (might be due to invalid IL or missing references)
//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
//IL_0055: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)ZNet.instance == (Object)null)
{
BN_Logger.LogInfo($"Steamworks: Unable to set net config while disconnected: {valueType}");
return;
}
int steamNetworkConfig = GetSteamNetworkConfig(valueType);
GCHandle gCHandle = GCHandle.Alloc(value, GCHandleType.Pinned);
try
{
if (BN_Utils.isDedicated)
{
SteamGameServerNetworkingUtils.SetConfigValue(valueType, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject());
}
else
{
SteamNetworkingUtils.SetConfigValue(valueType, (ESteamNetworkingConfigScope)1, IntPtr.Zero, (ESteamNetworkingConfigDataType)1, gCHandle.AddrOfPinnedObject());
}
}
catch
{
BN_Logger.LogError($"Steamworks: Unable to set net config: {valueType}");
}
gCHandle.Free();
BN_Logger.LogMessage($"Steamworks: {valueType}: {steamNetworkConfig} -> {GetSteamNetworkConfig(valueType)} (attempted {value})");
}
[HarmonyPatch("SetConfigValue")]
private static void Prefix(ESteamNetworkingConfigValue eValue, ESteamNetworkingConfigScope eScopeType, IntPtr scopeObj, ESteamNetworkingConfigDataType eDataType, ref IntPtr pArg)
{
//IL_0005: Unknown result type (might be due to invalid IL or missing references)
//IL_000b: Unknown result type (might be due to invalid IL or missing references)
BN_Logger.LogInfo($"Steamworks: {eValue}: {GetSteamNetworkConfig(eValue)} -> {Marshal.ReadInt32(pArg)}");
}
}
[HarmonyPatch(typeof(ZSteamSocket), "RegisterGlobalCallbacks")]
private class PreventValheimControlOfNetworkRate_Patch
{
private static void Postfix()
{
NetworkSendRate_Patch.SetSendRateMinFromConfig();
NetworkSendRate_Patch.SetSendRateMaxFromConfig();
}
}
private const int DEFAULT_SEND_BUFFER_SIZE = 524288;
private const int SEND_BUFFER_SIZE = 524288;
public static void InitConfig(ConfigFile config)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_0047: Expected O, but got Unknown
BetterNetworking.configNetworkSendRateMin = config.Bind<Options_NetworkSendRateMin>("Networking (Steamworks)", "Minimum Send Rate", Options_NetworkSendRateMin._256KB, new ConfigDescription("Steamworks: The minimum speed Steam will <i>attempt</i> to send data.\n<b>Lower this below your internet upload speed.</b>\n", (AcceptableValueBase)null, Array.Empty<object>()));
BetterNetworking.configNetworkSendRateMax = config.Bind<Options_NetworkSendRateMax>("Networking (Steamworks)", "Maximum Send Rate", Options_NetworkSendRateMax._1024KB, new ConfigDescription("Steamworks: The maximum speed Steam will <i>attempt</i> to send data.\nIf you have a low upload speed, lower this <i>below</i> your internet upload speed.\n", (AcceptableValueBase)null, Array.Empty<object>()));
ConfigNetworkSendRateSettings_Listen();
}
public static void ConfigNetworkSendRateSettings_Listen()
{
BetterNetworking.configNetworkSendRateMin.SettingChanged += ConfigNetworkSendRateMin_SettingChanged;
BetterNetworking.configNetworkSendRateMax.SettingChanged += ConfigNetworkSendRateMax_SettingChanged;
}
private static void ConfigNetworkSendRateMin_SettingChanged(object sender, EventArgs e)
{
if ((int)(BetterNetworking.configNetworkSendRateMin.Value + 1) < (int)BetterNetworking.configNetworkSendRateMax.Value)
{
BetterNetworking.configNetworkSendRateMax.Value = (Options_NetworkSendRateMax)(BetterNetworking.configNetworkSendRateMin.Value + 1);
}
NetworkSendRate_Patch.SetSendRateMinFromConfig();
}
private static void ConfigNetworkSendRateMax_SettingChanged(object sender, EventArgs e)
{
if ((int)BetterNetworking.configNetworkSendRateMax.Value > (int)(BetterNetworking.configNetworkSendRateMin.Value + 1))
{
BetterNetworking.configNetworkSendRateMin.Value = (Options_NetworkSendRateMin)(BetterNetworking.configNetworkSendRateMax.Value - 1);
}
NetworkSendRate_Patch.SetSendRateMaxFromConfig();
}
}
public class BN_Patch_UpdateRate
{
public enum Options_NetworkUpdateRates
{
[Description("100% <b>[default]</b>")]
_100,
[Description("75%")]
_75,
[Description("50%")]
_50
}
[HarmonyPatch(typeof(ZDOMan))]
private class NetworkUpdateFrequency_Patch
{
[HarmonyPatch("SendZDOToPeers2")]
private static void Prefix(ref float dt)
{
switch (BetterNetworking.configNetworkUpdateRate.Value)
{
case Options_NetworkUpdateRates._75:
dt *= 0.75f;
break;
case Options_NetworkUpdateRates._50:
dt *= 0.5f;
break;
}
}
}
public static void InitConfig(ConfigFile config)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
BetterNetworking.configNetworkUpdateRate = config.Bind<Options_NetworkUpdateRates>("Networking", "Update Rate", Options_NetworkUpdateRates._100, new ConfigDescription("Reducing this can help if your upload speed is low.\n100%: 20 updates/second\n75%: 15 updates/second\n50%: 10 updates/second", (AcceptableValueBase)null, Array.Empty<object>()));
}
}
[HarmonyPatch]
public class BN_Patch_Compression
{
public enum Options_NetworkCompression
{
[Description("Enabled <b>[default]</b>")]
@true,
[Description("Disabled")]
@false
}
private static string ZSTD_DICT_RESOURCE_NAME = "CW_Jesse.BetterNetworking.dict.small";
private static int ZSTD_LEVEL = 1;
private static Compressor compressor;
private static Decompressor decompressor;
private const string RPC_COMPRESSION_VERSION = "CW_Jesse.BetterNetworking.CompressionVersion";
private const string RPC_COMPRESSION_ENABLED = "CW_Jesse.BetterNetworking.CompressionEnabled";
private const string RPC_COMPRESSION_STARTED = "CW_Jesse.BetterNetworking.CompressedStarted";
public static void InitCompressor()
{
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_0048: Expected O, but got Unknown
//IL_0053: Unknown result type (might be due to invalid IL or missing references)
//IL_005d: Expected O, but got Unknown
byte[] array;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ZSTD_DICT_RESOURCE_NAME))
{
array = new byte[stream.Length];
stream.Read(array, 0, (int)stream.Length);
}
compressor = new Compressor(ZSTD_LEVEL);
compressor.LoadDictionary(array);
decompressor = new Decompressor();
decompressor.LoadDictionary(array);
}
public static void InitConfig(ConfigFile config)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
BetterNetworking.configCompressionEnabled = config.Bind<Options_NetworkCompression>("Networking", "Compression Enabled", Options_NetworkCompression.@true, new ConfigDescription("Keep this enabled unless comparing difference.\n---\nCrossplay enabled: Increases speed and strength of network compression.\nCrossplay disabled: Adds network compression.", (AcceptableValueBase)null, Array.Empty<object>()));
BetterNetworking.configCompressionEnabled.SettingChanged += ConfigCompressionEnabled_SettingChanged;
}
private static void ConfigCompressionEnabled_SettingChanged(object sender, EventArgs e)
{
SetCompressionEnabledFromConfig();
}
private static void SetCompressionEnabledFromConfig()
{
bool compressionEnabled;
if (BetterNetworking.configCompressionEnabled.Value == Options_NetworkCompression.@true)
{
compressionEnabled = true;
BN_Logger.LogMessage("Compression: Enabling");
}
else
{
compressionEnabled = false;
BN_Logger.LogMessage("Compression: Disabling");
}
CompressionStatus.ourStatus.compressionEnabled = compressionEnabled;
SendCompressionEnabledStatus();
}
[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
[HarmonyPostfix]
private static void OnConnect(ref ZNetPeer peer)
{
CompressionStatus.AddSocket(peer.m_socket);
RegisterRPCs(peer);
SendCompressionVersion(peer);
}
[HarmonyPatch(typeof(ZNet), "Disconnect")]
[HarmonyPrefix]
private static void OnDisconnectPrefix(ZNetPeer peer)
{
BN_Logger.LogMessage("Compression: " + BN_Utils.GetPeerName(peer) + " disconnected");
}
[HarmonyPatch(typeof(ZNet), "Disconnect")]
[HarmonyPostfix]
private static void OnDisconnectPostfix(ZNetPeer peer)
{
CompressionStatus.RemoveSocket(peer.m_socket);
}
internal static byte[] Compress(byte[] buffer)
{
byte[] array = compressor.Wrap((ReadOnlySpan<byte>)buffer).ToArray();
if (BetterNetworking.configLogMessages.Value >= BN_Logger.Options_Logger_LogLevel.info && buffer.Length > 256)
{
float num = (float)array.Length / (float)buffer.Length * 100f;
BN_Logger.LogInfo(string.Format("Compression: Sent {0} B compressed to {1}%", buffer.Length, num.ToString("0")));
}
return array;
}
internal static byte[] Decompress(byte[] compressedBuffer)
{
byte[] array = decompressor.Unwrap((ReadOnlySpan<byte>)compressedBuffer, int.MaxValue).ToArray();
if (BetterNetworking.configLogMessages.Value >= BN_Logger.Options_Logger_LogLevel.info && array.Length > 256)
{
float num = (float)compressedBuffer.Length / (float)array.Length * 100f;
BN_Logger.LogInfo(string.Format("Compression: Received {0} B compressed to {1}%", array.Length, num.ToString("0")));
}
return array;
}
private static void RegisterRPCs(ZNetPeer peer)
{
peer.m_rpc.Register<int>("CW_Jesse.BetterNetworking.CompressionVersion", (Action<ZRpc, int>)RPC_CompressionVersion);
peer.m_rpc.Register<bool>("CW_Jesse.BetterNetworking.CompressionEnabled", (Action<ZRpc, bool>)RPC_CompressionEnabled);
peer.m_rpc.Register<bool>("CW_Jesse.BetterNetworking.CompressedStarted", (Action<ZRpc, bool>)RPC_CompressionStarted);
}
public static void SendCompressionVersion(ZNetPeer peer)
{
peer.m_rpc.Invoke("CW_Jesse.BetterNetworking.CompressionVersion", new object[1] { CompressionStatus.ourStatus.version });
}
private static void RPC_CompressionVersion(ZRpc rpc, int version)
{
ZNetPeer peer = BN_Utils.GetPeer(rpc);
CompressionStatus.SetVersion(peer.m_socket, version);
if (CompressionStatus.ourStatus.version == version)
{
BN_Logger.LogMessage("Compression: Compatible with " + BN_Utils.GetPeerName(peer));
}
else if (CompressionStatus.ourStatus.version > version)
{
BN_Logger.LogWarning($"Compression: {BN_Utils.GetPeerName(peer)} ({version}) has an older version of Better Networking; they should update");
}
else if (version > 0)
{
BN_Logger.LogError($"Compression: {BN_Utils.GetPeerName(peer)} ({version}) has a newer version of Better Networking; you should update");
}
if (CompressionStatus.GetIsCompatibleWith(peer.m_socket))
{
SendCompressionEnabledStatus(peer);
}
}
private static void SendCompressionEnabledStatus()
{
if ((Object)(object)ZNet.instance == (Object)null)
{
return;
}
foreach (ZNetPeer peer in ZNet.instance.GetPeers())
{
if (CompressionStatus.GetIsCompatibleWith(peer.m_socket))
{
SendCompressionEnabledStatus(peer);
}
}
}
private static void SendCompressionEnabledStatus(ZNetPeer peer)
{
if (!((Object)(object)ZNet.instance == (Object)null))
{
peer.m_rpc.Invoke("CW_Jesse.BetterNetworking.CompressionEnabled", new object[1] { CompressionStatus.ourStatus.compressionEnabled });
if (CompressionStatus.ourStatus.compressionEnabled && CompressionStatus.GetCompressionEnabled(peer.m_socket))
{
SendCompressionStarted(peer, started: true);
}
else
{
SendCompressionStarted(peer, started: false);
}
}
}
private static void RPC_CompressionEnabled(ZRpc rpc, bool peerCompressionEnabled)
{
ZNetPeer peer = BN_Utils.GetPeer(rpc);
CompressionStatus.SetCompressionEnabled(peer.m_socket, peerCompressionEnabled);
if (CompressionStatus.ourStatus.compressionEnabled && peerCompressionEnabled)
{
SendCompressionStarted(peer, started: true);
}
else
{
SendCompressionStarted(peer, started: false);
}
}
private static void SendCompressionStarted(ZNetPeer peer, bool started)
{
if (!((Object)(object)ZNet.instance == (Object)null) && CompressionStatus.GetSendCompressionStarted(peer.m_socket) != started)
{
peer.m_rpc.Invoke("CW_Jesse.BetterNetworking.CompressedStarted", new object[1] { started });
Flush(peer);
CompressionStatus.SetSendCompressionStarted(peer.m_socket, started);
BN_Logger.LogMessage($"Compression: Compression to {BN_Utils.GetPeerName(peer)}: {started}");
}
}
private static void Flush(ZNetPeer peer)
{
//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_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_0009: Unknown result type (might be due to invalid IL or missing references)
//IL_000b: Invalid comparison between Unknown and I4
OnlineBackendType onlineBackend = ZNet.m_onlineBackend;
if ((int)onlineBackend != 0)
{
if ((int)onlineBackend == 1)
{
BN_Patch_Compression_PlayFab.FlushQueue(peer.m_socket);
}
}
else
{
peer.m_socket.Flush();
}
}
private static void RPC_CompressionStarted(ZRpc rpc, bool peerCompressionStarted)
{
ZNetPeer peer = BN_Utils.GetPeer(rpc);
CompressionStatus.SetReceiveCompressionStarted(peer.m_socket, peerCompressionStarted);
BN_Logger.LogMessage($"Compression: Compression from {BN_Utils.GetPeerName(peer)}: {peerCompressionStarted}");
}
}
[HarmonyPatch]
public static class BN_Patch_Compression_PlayFab
{
private static Dictionary<PlayFabZLibWorkQueue, ZPlayFabSocket> workQueueSockets = new Dictionary<PlayFabZLibWorkQueue, ZPlayFabSocket>();
[HarmonyPatch(/*Could not decode attribute arguments.*/)]
[HarmonyPostfix]
private static void SocketOpen0(ref ZPlayFabSocket __instance, ref PlayFabZLibWorkQueue ___m_zlibWorkQueue)
{
SocketOpen(ref __instance, ref ___m_zlibWorkQueue);
}
[HarmonyPatch(/*Could not decode attribute arguments.*/)]
[HarmonyPostfix]
private static void SocketOpen1(ref ZPlayFabSocket __instance, ref PlayFabZLibWorkQueue ___m_zlibWorkQueue)
{
SocketOpen(ref __instance, ref ___m_zlibWorkQueue);
}
[HarmonyPatch(/*Could not decode attribute arguments.*/)]
[HarmonyPostfix]
private static void SocketOpen2(ref ZPlayFabSocket __instance, ref PlayFabZLibWorkQueue ___m_zlibWorkQueue)
{
SocketOpen(ref __instance, ref ___m_zlibWorkQueue);
}
private static void SocketOpen(ref ZPlayFabSocket __instance, ref PlayFabZLibWorkQueue ___m_zlibWorkQueue)
{
workQueueSockets.Add(___m_zlibWorkQueue, __instance);
BN_Logger.LogMessage("PlayFab: Added socket " + BN_Utils.GetPeerName((ISocket)(object)__instance));
}
[HarmonyPatch(typeof(ZPlayFabSocket), "Dispose")]
[HarmonyPostfix]
private static void SocketClose(ref PlayFabZLibWorkQueue ___m_zlibWorkQueue)
{
workQueueSockets.Remove(___m_zlibWorkQueue);
BN_Logger.LogMessage("PlayFab: Removed socket");
}
[HarmonyPatch(typeof(PlayFabZLibWorkQueue), "DoCompress")]
[HarmonyPrefix]
private static bool PlayFab_Compress(ref PlayFabZLibWorkQueue __instance, ref Queue<byte[]> ___m_inCompress, ref Queue<byte[]> ___m_outCompress)
{
if (!workQueueSockets.TryGetValue(__instance, out var value))
{
return true;
}
if (!CompressionStatus.GetSendCompressionStarted((ISocket)(object)value))
{
return true;
}
while (___m_inCompress.Count > 0)
{
try
{
___m_outCompress.Enqueue(BN_Patch_Compression.Compress(___m_inCompress.Dequeue()));
}
catch
{
BN_Logger.LogError("PlayFab: Failed BN compress");
}
}
return false;
}
[HarmonyPatch(typeof(PlayFabZLibWorkQueue), "DoUncompress")]
[HarmonyPrefix]
private static bool PlayFab_Decompress(ref PlayFabZLibWorkQueue __instance, ref Queue<byte[]> ___m_inDecompress, ref Queue<byte[]> ___m_outDecompress)
{
bool flag = false;
if (workQueueSockets.TryGetValue(__instance, out var value))
{
flag = CompressionStatus.GetReceiveCompressionStarted((ISocket)(object)value);
}
while (___m_inDecompress.Count > 0)
{
byte[] array = ___m_inDecompress.Dequeue();
try
{
___m_outDecompress.Enqueue(BN_Patch_Compression.Decompress(array));
if (!CompressionStatus.GetReceiveCompressionStarted((ISocket)(object)value))
{
BN_Logger.LogMessage("PlayFab: Received unexpected compressed message from " + BN_Utils.GetPeerName((ISocket)(object)value));
CompressionStatus.SetReceiveCompressionStarted((ISocket)(object)value, started: true);
}
}
catch
{
if (flag)
{
BN_Logger.LogInfo("PlayFab: Failed BN decompress");
}
try
{
___m_outDecompress.Enqueue((byte[])AccessTools.Method(typeof(PlayFabZLibWorkQueue), "UncompressOnThisThread", (Type[])null, (Type[])null).Invoke(__instance, new object[1] { array }));
if (CompressionStatus.GetReceiveCompressionStarted((ISocket)(object)value))
{
BN_Logger.LogMessage("PlayFab: Received unexpected vanilla message from " + BN_Utils.GetPeerName((ISocket)(object)value));
CompressionStatus.SetReceiveCompressionStarted((ISocket)(object)value, started: false);
}
}
catch
{
BN_Logger.LogMessage("PlayFab: Failed vanilla decompress; keeping data (this data would have been lost without Better Networking)");
___m_outDecompress.Enqueue(array);
}
}
}
return false;
}
public static void FlushQueue(ISocket socket)
{
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0020: Expected O, but got Unknown
PlayFabZLibWorkQueue obj = (PlayFabZLibWorkQueue)AccessTools.Field(typeof(ZPlayFabSocket), "m_zlibWorkQueue").GetValue(socket);
Mutex mutex = (Mutex)AccessTools.Field(typeof(PlayFabZLibWorkQueue), "s_workersMutex").GetValue(obj);
List<PlayFabZLibWorkQueue> obj2 = (List<PlayFabZLibWorkQueue>)AccessTools.Field(typeof(PlayFabZLibWorkQueue), "s_workers").GetValue(obj);
MethodInfo methodInfo = AccessTools.Method(typeof(PlayFabZLibWorkQueue), "Execute", (Type[])null, (Type[])null);
mutex.WaitOne();
foreach (PlayFabZLibWorkQueue item in obj2)
{
methodInfo.Invoke(item, new object[0]);
}
mutex.ReleaseMutex();
AccessTools.Method(typeof(ZPlayFabSocket), "LateUpdate", (Type[])null, (Type[])null).Invoke(socket, null);
}
}
[HarmonyPatch]
public static class BN_Patch_Compression_Steamworks
{
private const int k_nSteamNetworkingSend_Reliable = 8;
private const int k_cbMaxSteamNetworkingSocketsMessageSizeSend = 524288;
[HarmonyPatch(typeof(ZSteamSocket), "SendQueuedPackages")]
[HarmonyPrefix]
private static bool Steamworks_SendCompressedPackages(ref ZSteamSocket __instance, ref Queue<byte[]> ___m_sendQueue)
{
if (!__instance.IsConnected())
{
return false;
}
if (!CompressionStatus.GetSendCompressionStarted((ISocket)(object)__instance))
{
return true;
}
___m_sendQueue = new Queue<byte[]>(___m_sendQueue.Select((byte[] p) => BN_Patch_Compression.Compress(p)));
return true;
}
[HarmonyPatch(typeof(ZSteamSocket), "Recv")]
[HarmonyPostfix]
private static void Steamworks_ReceiveCompressedPackages(ref ZPackage __result, ref ZSteamSocket __instance)
{
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Expected O, but got Unknown
if (!__instance.IsConnected() || __result == null)
{
return;
}
try
{
byte[] array = BN_Patch_Compression.Decompress(__result.GetArray());
__result = new ZPackage(array);
if (!CompressionStatus.GetReceiveCompressionStarted((ISocket)(object)__instance))
{
BN_Logger.LogMessage("Compression (Steamworks): Received unexpected compressed message from " + BN_Utils.GetPeerName((ISocket)(object)__instance));
CompressionStatus.SetReceiveCompressionStarted((ISocket)(object)__instance, started: true);
}
}
catch
{
if (CompressionStatus.GetReceiveCompressionStarted((ISocket)(object)__instance))
{
BN_Logger.LogMessage("Compression (Steamworks): Received unexpected uncompressed message from " + BN_Utils.GetPeerName((ISocket)(object)__instance));
CompressionStatus.SetReceiveCompressionStarted((ISocket)(object)__instance, started: false);
}
}
}
}
internal static class CompressionStatus
{
public class SocketCompressionStatus
{
public int version;
public bool compressionEnabled;
public bool receivingCompressed;
public bool sendingCompressed;
}
private const int COMPRESSION_VERSION = 6;
private const int COMPRESSION_VERSION_UNKNOWN = 0;
public static SocketCompressionStatus ourStatus = new SocketCompressionStatus
{
version = 6,
compressionEnabled = (BetterNetworking.configCompressionEnabled.Value == BN_Patch_Compression.Options_NetworkCompression.@true)
};
private static readonly Dictionary<ISocket, SocketCompressionStatus> socketStatuses = new Dictionary<ISocket, SocketCompressionStatus>();
public static bool AddSocket(ISocket socket)
{
if (socket == null)
{
BN_Logger.LogWarning("Compression: Tried to add null peer");
return false;
}
if (IsSocketExist(socket))
{
BN_Logger.LogWarning("Compression: Removing existing peer (" + BN_Utils.GetPeerName(socket) + "); did they lose internet or Alt+F4?");
RemoveSocket(socket);
}
BN_Logger.LogMessage("Compression: " + BN_Utils.GetPeerName(socket) + " connected");
socketStatuses.Add(socket, new SocketCompressionStatus());
return true;
}
public static void RemoveSocket(ISocket socket)
{
if (!IsSocketExist(socket))
{
BN_Logger.LogWarning("Compression: Tried to remove non-existent peer: " + BN_Utils.GetPeerName(socket));
}
else
{
socketStatuses.Remove(socket);
}
}
public static bool IsSocketExist(ISocket socket)
{
if (socket != null && socketStatuses.ContainsKey(socket))
{
return true;
}
return false;
}
public static int GetVersion(ISocket socket)
{
if (!IsSocketExist(socket))
{
return 0;
}
return socketStatuses[socket].version;
}
public static void SetVersion(ISocket socket, int theirVersion)
{
if (IsSocketExist(socket))
{
socketStatuses[socket].version = theirVersion;
}
}
public static bool GetIsCompatibleWith(ISocket socket)
{
if (!IsSocketExist(socket))
{
return false;
}
return ourStatus.version == GetVersion(socket);
}
public static bool GetCompressionEnabled(ISocket socket)
{
if (!IsSocketExist(socket))
{
return false;
}
return socketStatuses[socket].compressionEnabled;
}
public static void SetCompressionEnabled(ISocket socket, bool enabled)
{
if (IsSocketExist(socket))
{
socketStatuses[socket].compressionEnabled = enabled;
}
}
public static bool GetSendCompressionStarted(ISocket socket)
{
if (!IsSocketExist(socket))
{
return false;
}
return socketStatuses[socket].sendingCompressed;
}
public static void SetSendCompressionStarted(ISocket socket, bool started)
{
if (IsSocketExist(socket))
{
socketStatuses[socket].sendingCompressed = started;
}
}
public static bool GetReceiveCompressionStarted(ISocket socket)
{
if (!IsSocketExist(socket))
{
return false;
}
return socketStatuses[socket].receivingCompressed;
}
public static void SetReceiveCompressionStarted(ISocket socket, bool started)
{
if (IsSocketExist(socket))
{
socketStatuses[socket].receivingCompressed = started;
}
}
}
}
namespace Costura
{
[CompilerGenerated]
internal static class AssemblyLoader
{
private static object nullCacheLock = new object();
private static Dictionary<string, bool> nullCache = new Dictionary<string, bool>();
private static Dictionary<string, string> assemblyNames = new Dictionary<string, string>();
private static Dictionary<string, string> symbolNames = new Dictionary<string, string>();
private static int isAttached;
private static string CultureToString(CultureInfo culture)
{
if (culture == null)
{
return "";
}
return culture.Name;
}
private static Assembly ReadExistingAssembly(AssemblyName name)
{
AppDomain currentDomain = AppDomain.CurrentDomain;
Assembly[] assemblies = currentDomain.GetAssemblies();
Assembly[] array = assemblies;
foreach (Assembly assembly in array)
{
AssemblyName name2 = assembly.GetName();
if (string.Equals(name2.Name, name.Name, StringComparison.InvariantCultureIgnoreCase) && string.Equals(CultureToString(name2.CultureInfo), CultureToString(name.CultureInfo), StringComparison.InvariantCultureIgnoreCase))
{
return assembly;
}
}
return null;
}
private static void CopyTo(Stream source, Stream destination)
{
byte[] array = new byte[81920];
int count;
while ((count = source.Read(array, 0, array.Length)) != 0)
{
destination.Write(array, 0, count);
}
}
private static Stream LoadStream(string fullName)
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
if (fullName.EndsWith(".compressed"))
{
using (Stream stream = executingAssembly.GetManifestResourceStream(fullName))
{
using DeflateStream source = new DeflateStream(stream, CompressionMode.Decompress);
MemoryStream memoryStream = new MemoryStream();
CopyTo(source, memoryStream);
memoryStream.Position = 0L;
return memoryStream;
}
}
return executingAssembly.GetManifestResourceStream(fullName);
}
private static Stream LoadStream(Dictionary<string, string> resourceNames, string name)
{
if (resourceNames.TryGetValue(name, out var value))
{
return LoadStream(value);
}
return null;
}
private static byte[] ReadStream(Stream stream)
{
byte[] array = new byte[stream.Length];
stream.Read(array, 0, array.Length);
return array;
}
private static Assembly ReadFromEmbeddedResources(Dictionary<string, string> assemblyNames, Dictionary<string, string> symbolNames, AssemblyName requestedAssemblyName)
{
string text = requestedAssemblyName.Name.ToLowerInvariant();
if (requestedAssemblyName.CultureInfo != null && !string.IsNullOrEmpty(requestedAssemblyName.CultureInfo.Name))
{
text = requestedAssemblyName.CultureInfo.Name + "." + text;
}
byte[] rawAssembly;
using (Stream stream = LoadStream(assemblyNames, text))
{
if (stream == null)
{
return null;
}
rawAssembly = ReadStream(stream);
}
using (Stream stream2 = LoadStream(symbolNames, text))
{
if (stream2 != null)
{
byte[] rawSymbolStore = ReadStream(stream2);
return Assembly.Load(rawAssembly, rawSymbolStore);
}
}
return Assembly.Load(rawAssembly);
}
public static Assembly ResolveAssembly(object sender, ResolveEventArgs e)
{
lock (nullCacheLock)
{
if (nullCache.ContainsKey(e.Name))
{
return null;
}
}
AssemblyName assemblyName = new AssemblyName(e.Name);
Assembly assembly = ReadExistingAssembly(assemblyName);
if ((object)assembly != null)
{
return assembly;
}
assembly = ReadFromEmbeddedResources(assemblyNames, symbolNames, assemblyName);
if ((object)assembly == null)
{
lock (nullCacheLock)
{
nullCache[e.Name] = true;
}
if ((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0)
{
assembly = Assembly.Load(assemblyName);
}
}
return assembly;
}
static AssemblyLoader()
{
assemblyNames.Add("costura", "costura.costura.dll.compressed");
symbolNames.Add("costura", "costura.costura.pdb.compressed");
assemblyNames.Add("microsoft.bcl.asyncinterfaces", "costura.microsoft.bcl.asyncinterfaces.dll.compressed");
assemblyNames.Add("system.buffers", "costura.system.buffers.dll.compressed");
assemblyNames.Add("system.diagnostics.diagnosticsource", "costura.system.diagnostics.diagnosticsource.dll.compressed");
assemblyNames.Add("system.memory", "costura.system.memory.dll.compressed");
assemblyNames.Add("system.numerics.vectors", "costura.system.numerics.vectors.dll.compressed");
assemblyNames.Add("system.runtime.compilerservices.unsafe", "costura.system.runtime.compilerservices.unsafe.dll.compressed");
assemblyNames.Add("system.threading.tasks.extensions", "costura.system.threading.tasks.extensions.dll.compressed");
assemblyNames.Add("zstdsharp", "costura.zstdsharp.dll.compressed");
}
public static void Attach()
{
if (Interlocked.Exchange(ref isAttached, 1) == 1)
{
return;
}
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)
{
lock (nullCacheLock)
{
if (nullCache.ContainsKey(e.Name))
{
return null;
}
}
AssemblyName assemblyName = new AssemblyName(e.Name);
Assembly assembly = ReadExistingAssembly(assemblyName);
if ((object)assembly != null)
{
return assembly;
}
assembly = ReadFromEmbeddedResources(assemblyNames, symbolNames, assemblyName);
if ((object)assembly == null)
{
lock (nullCacheLock)
{
nullCache[e.Name] = true;
}
if ((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0)
{
assembly = Assembly.Load(assemblyName);
}
}
return assembly;
};
}
}
}
internal class CW_JesseBetterNetworking_ProcessedByFody
{
internal const string FodyVersion = "6.6.4.0";
internal const string Costura = "5.7.0";
}