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 ValheimOne v0.13.29
plugins/ValheimOne.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Sockets; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.UI; using ValheimOne.ActivityLog; using ValheimOne.Configuration; using ValheimOne.Discord; using ValheimOne.Infrastructure; using ValheimOne.LiveMap; using ValheimOne.Modules; using ValheimOne.Networking; using ValheimOne.Query; [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("ValheimOne")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.13.29.0")] [assembly: AssemblyInformationalVersion("0.13.29")] [assembly: AssemblyProduct("ValheimOne")] [assembly: AssemblyTitle("ValheimOne")] [assembly: InternalsVisibleTo("ValheimOne.RuntimeRegression")] [assembly: AssemblyVersion("0.13.29.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace ValheimOne { [BepInPlugin("com.humangenome.valheimone", "ValheimOne", "0.13.29")] public sealed class ValheimOnePlugin : BaseUnityPlugin { private const int ConfigReloadDebounceMilliseconds = 750; public const string PluginGuid = "com.humangenome.valheimone"; public const string PluginName = "ValheimOne"; public const string PluginVersion = "0.13.29"; private Harmony? _harmony; private ConfigHotReloadWatcher? _configWatcher; private IVersionHandshake? _versionHandshake; private ActivityLogModule? _activityLogModule; private LiveMapModule? _liveMapModule; private DiscordModule? _discordModule; private NetworkModule? _networkModule; private ServerSessionEventSource? _sessionEvents; private ValheimOneConfig? _settings; private ModLogger? _log; private void Awake() { //IL_02d8: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: Expected O, but got Unknown _log = new ModLogger(((BaseUnityPlugin)this).Logger); LogStartupBanner(_log); string text = Path.Combine(Paths.ConfigPath, "valheimone.cfg"); ValheimOneConfig valheimOneConfig = (_settings = new ValheimOneConfig(text)); ServerConfig serverConfig = new ServerConfig(valheimOneConfig.Features); MapSharingModule mapSharingModule = new MapSharingModule(valheimOneConfig.Features, _log); ServerSessionEventSource sessionEvents = (_sessionEvents = ServerSessionEventSource.Initialize(((Component)this).gameObject, _log)); string text2 = Path.Combine(Paths.ConfigPath, "ValheimOne"); string text3 = WorldDataDirectory.Resolve(text2, _log); ActivityLogModule activityLogModule = (_activityLogModule = new ActivityLogModule(valheimOneConfig.Features, text3, sessionEvents, _log)); DiscordModule discordModule = (_discordModule = new DiscordModule(valheimOneConfig.Features, sessionEvents, _log)); LiveMapModule liveMapModule = (_liveMapModule = new LiveMapModule(valheimOneConfig.Features, activityLogModule, sessionEvents, text2, text3)); NetworkModule networkModule = (_networkModule = new NetworkModule(valheimOneConfig.Features, _log)); IReadOnlyList<IFeatureModule> readOnlyList = new IFeatureModule[32] { new PlayerModule(valheimOneConfig.Features), new PlayerStaminaModule(valheimOneConfig.Features), new BuildingQoLModule(valheimOneConfig.Features), new StructuralIntegrityModule(valheimOneConfig.Features), new FoodDurationModule(valheimOneConfig.Features), new ItemTweaksModule(valheimOneConfig.Features), new ItemDropMultiplierModule(valheimOneConfig.Features), new GatheringModule(valheimOneConfig.Features), new TamesModule(valheimOneConfig.Features), new WorldEventsModule(valheimOneConfig.Features), new DayNightLengthModule(valheimOneConfig.Features), new TraderModule(valheimOneConfig.Features), new BeehiveModule(valheimOneConfig.Features), new FireSourceModule(valheimOneConfig.Features, _log), new FermenterModule(valheimOneConfig.Features), new SapCollectorModule(valheimOneConfig.Features), new ProductionSpeedsModule(valheimOneConfig.Features, _log), new ContainerSizesModule(valheimOneConfig.Features, _log), new WardsModule(valheimOneConfig.Features), new PortalsModule(valheimOneConfig.Features), new CraftFromChestModule(valheimOneConfig.Features), new StationAutomationModule(valheimOneConfig.Features), new CookingStationModule(valheimOneConfig.Features), new ExperienceRatesModule(valheimOneConfig.Features), new DeathPenaltyModule(valheimOneConfig.Features), mapSharingModule, activityLogModule, liveMapModule, new QueryModule(valheimOneConfig.Features), discordModule, new ServerHostModule(serverConfig, _log), networkModule }; valheimOneConfig.WriteDefaultsIfNeeded(); _harmony = new Harmony("com.humangenome.valheimone"); List<string> list = (ContractDiagnostics.IsEnabled ? new List<string>() : null); int num = 0; foreach (IFeatureModule item in readOnlyList) { try { item.ApplyPatches(_harmony); valheimOneConfig.Features.RecordPatchSuccess(item.Section); num++; string arg = (item.IsEnabled ? "enabled" : "disabled"); _log.Info($"Feature patches ready: {item.Section} ({item.Classification}, {arg})."); } catch (Exception ex) { string text4 = ContractDiagnostics.DescribePatchFailure(item, ex); valheimOneConfig.Features.RecordPatchFailure(item.Section, text4); list?.Add(text4); _log.Error("Feature patch application failed: " + text4 + " (" + ex.GetType().Name + ": " + ContractDiagnostics.SingleLineMessage(ex) + "). Continuing with remaining modules."); } } _versionHandshake = new VersionHandshake(valheimOneConfig, serverConfig, _log, new IVersionHandshakeExtension[1] { mapSharingModule }); _versionHandshake.Initialize(_harmony); if (ContractDiagnostics.IsEnabled) { ContractDiagnostics.Initialize(_harmony, _log, readOnlyList, num, list); } _configWatcher = new ConfigHotReloadWatcher(text, _log); _configWatcher.Start(); _log.Info("Configuration: " + text); } private void Start() { _networkModule?.ReportStartupState(); } private void Update() { ConfigHotReloadWatcher configWatcher = _configWatcher; ValheimOneConfig settings = _settings; ModLogger log = _log; if (configWatcher == null || settings == null || log == null || !configWatcher.TryConsumeChange(750)) { return; } Dictionary<IConfigEntry, string> dictionary = SnapshotEffectiveValues(settings.Features); try { settings.File.Reload(); } catch (Exception ex) { log.Warning("Config hot-reload failed; effective values were not reapplied: " + ex.GetType().Name + ": " + ContractDiagnostics.SingleLineMessage(ex)); return; } Dictionary<IConfigEntry, string> dictionary2 = SnapshotEffectiveValues(settings.Features); bool flag = false; foreach (FeatureDefinition feature in settings.Features.Features) { foreach (IConfigEntry key in feature.Keys) { string text = dictionary[key]; string text2 = dictionary2[key]; if (!string.Equals(text, text2, StringComparison.Ordinal)) { flag = true; string text3 = ((key == feature.Enabled) ? " (feature gate — patches stay installed; enabling/disabling applies live, new patch topology requires restart)" : string.Empty); if (key.Definition.IsSensitive) { log.Info("Config hot-reload: [" + key.Definition.Section + "] " + key.Definition.Name + ": value changed (applied live)" + text3); } else { log.Info("Config hot-reload: [" + key.Definition.Section + "] " + key.Definition.Name + ": " + text + " -> " + text2 + " (applied live)" + text3); } } } } if (flag) { settings.Features.NotifyEffectiveValuesChanged(); } else { log.Debug("Config hot-reload: config file touched; no effective changes"); } } private void OnDestroy() { _configWatcher?.Dispose(); _configWatcher = null; _discordModule?.Shutdown(); _discordModule = null; _networkModule = null; _liveMapModule?.Shutdown(); _liveMapModule = null; _activityLogModule?.Shutdown(); _activityLogModule = null; _sessionEvents?.StopPermanently(); _sessionEvents = null; _settings = null; _versionHandshake?.Shutdown(); _versionHandshake = null; Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; } private static Dictionary<IConfigEntry, string> SnapshotEffectiveValues(FeatureRegistry registry) { Dictionary<IConfigEntry, string> dictionary = new Dictionary<IConfigEntry, string>(); foreach (FeatureDefinition feature in registry.Features) { foreach (IConfigEntry key in feature.Keys) { dictionary.Add(key, key.GetSerializedValue()); } } return dictionary; } private static void LogStartupBanner(ModLogger log) { string text = GameVersionDetector.TryDetect(log); string text2 = text ?? "unavailable"; log.Info("ValheimOne v0.13.29 starting | Valheim detected: " + text2 + " | supported: 1.0.21 | " + $"schema: {1}"); if (text != null && text.IndexOf("1.0.21", StringComparison.OrdinalIgnoreCase) < 0) { log.Warning("This build targets Valheim 1.0.21; detected " + text + ". Verify compatibility before enabling gameplay features."); } } } } namespace ValheimOne.Query { internal sealed class QueryBehaviour : MonoBehaviour { private const int DefaultGamePort = 2456; private const float SnapshotIntervalSeconds = 2f; private const float IdleSnapshotIntervalSeconds = 30f; private const float BindRetrySeconds = 30f; private QueryConfig? _config; private ModLogger? _log; private Func<bool>? _enabledCheck; private QueryResponder? _responder; private volatile QuerySnapshot _snapshot = QuerySnapshot.Empty; private ZNet? _sessionNetwork; private string _gameVersion = "unknown"; private DateTime _startTimeUtc; private float _nextSnapshotRefresh; private float _nextStartAttempt; private int _gamePort = 2456; private int _responderPort; private int _lastInvalidQueryPort = int.MinValue; private int _deprecatedMaxPlayers; private bool _deprecatedMaxPlayersChecked; private bool _passworded; private bool _versionDetected; private bool _sessionActive; private bool _idle; private bool _stopped; public static QueryBehaviour? Instance { get; private set; } public static void Initialize(GameObject host, QueryConfig config, ModLogger log, Func<bool> enabledCheck) { QueryBehaviour queryBehaviour = host.AddComponent<QueryBehaviour>(); queryBehaviour._config = config; queryBehaviour._log = log; queryBehaviour._enabledCheck = enabledCheck; queryBehaviour.ReadCommandLine(); Instance = queryBehaviour; } private void Update() { QueryConfig config = _config; ModLogger log = _log; if (_stopped || config == null || log == null || _enabledCheck == null) { return; } if (!_enabledCheck()) { StopSession(); return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { StopSession(); return; } World world = QueryGameAccess.GetWorld(); if (world == null) { StopSession(); return; } if (!_sessionActive || _sessionNetwork != instance) { StopSession(); StartSession(instance, log); } float realtimeSinceStartup = Time.realtimeSinceStartup; bool flag = QueryGameAccess.GetPeers(instance).Count == 0; if (flag != _idle) { RefreshSnapshot(instance, world, config); _idle = flag; _nextSnapshotRefresh = realtimeSinceStartup + (flag ? 30f : 2f); } else if (realtimeSinceStartup >= _nextSnapshotRefresh) { RefreshSnapshot(instance, world, config); _nextSnapshotRefresh = realtimeSinceStartup + (_idle ? 30f : 2f); } int effectiveQueryPort = GetEffectiveQueryPort(config, log); if (_responder == null || _responderPort != effectiveQueryPort) { StopResponder(); _responder = new QueryResponder(effectiveQueryPort, () => _snapshot, log); _responderPort = effectiveQueryPort; _nextStartAttempt = 0f; } if (!_responder.IsRunning && realtimeSinceStartup >= _nextStartAttempt && !_responder.Start()) { _nextStartAttempt = realtimeSinceStartup + 30f; } } private void StartSession(ZNet network, ModLogger log) { _sessionNetwork = network; _startTimeUtc = DateTime.UtcNow; _nextSnapshotRefresh = 0f; _nextStartAttempt = 0f; _idle = QueryGameAccess.GetPeers(network).Count == 0; _sessionActive = true; if (!_versionDetected) { _gameVersion = GameVersionDetector.TryDetect(log) ?? "unknown"; _versionDetected = true; } } private void RefreshSnapshot(ZNet network, World world, QueryConfig config) { string text = (string.IsNullOrWhiteSpace(world.m_name) ? "world" : world.m_name); string text2 = QueryGameAccess.GetServerName(); if (string.IsNullOrWhiteSpace(text2)) { text2 = text; } int num = ServerHostModule.EffectiveMaxPlayers(); if (num == 10) { int deprecatedQueryMaxPlayers = GetDeprecatedQueryMaxPlayers(); if (deprecatedQueryMaxPlayers > 0) { num = deprecatedQueryMaxPlayers; } } List<string> list = new List<string>(); List<ZNetPeer> peers = QueryGameAccess.GetPeers(network); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && !((ZDOID)(ref val.m_characterID)).IsNone()) { string item = (config.PublicPlayerNames ? (val.m_playerName ?? string.Empty) : $"Player {list.Count + 1}"); list.Add(item); } } string[] array = list.ToArray(); _snapshot = new QuerySnapshot(text2, text, array.Length, array, num, _gamePort, _passworded, _gameVersion, "0.13.29", _startTimeUtc); } private int GetDeprecatedQueryMaxPlayers() { if (_deprecatedMaxPlayersChecked) { return _deprecatedMaxPlayers; } _deprecatedMaxPlayersChecked = true; try { string path = Path.Combine(Paths.ConfigPath, "valheimone.cfg"); if (!File.Exists(path)) { return 0; } bool flag = false; string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.StartsWith("[", StringComparison.Ordinal)) { flag = string.Equals(text, "[Query]", StringComparison.OrdinalIgnoreCase); } else if (flag && text.StartsWith("MaxPlayers", StringComparison.OrdinalIgnoreCase)) { int num = text.IndexOf('='); if (num >= 0 && int.TryParse(text.Substring(num + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0 && result != 10) { _deprecatedMaxPlayers = Math.Min(result, 255); _log?.Warning("[Query] MaxPlayers is deprecated; move the value to [Server] MaxPlayers. This fallback will be removed in the next release."); break; } } } } catch (Exception) { } return _deprecatedMaxPlayers; } private int GetEffectiveQueryPort(QueryConfig config, ModLogger log) { int queryPort = config.QueryPort; if (queryPort == 0) { _lastInvalidQueryPort = int.MinValue; return _gamePort + 4; } if (queryPort >= 1 && queryPort <= 65535) { _lastInvalidQueryPort = int.MinValue; return queryPort; } if (_lastInvalidQueryPort != queryPort) { log.Warning($"[Query] QueryPort {queryPort} is invalid; using game port + 4 ({_gamePort + 4})."); _lastInvalidQueryPort = queryPort; } return _gamePort + 4; } private void ReadCommandLine() { string[] commandLineArgs = Environment.GetCommandLineArgs(); if (int.TryParse(GetCommandLineValue(commandLineArgs, "-port"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result >= 1 && result <= 65531) { _gamePort = result; } _passworded = !string.IsNullOrEmpty(GetCommandLineValue(commandLineArgs, "-password")); } private static string GetCommandLineValue(string[] args, string name) { string text = name + "="; for (int i = 0; i < args.Length; i++) { string text2 = args[i] ?? string.Empty; if (text2.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { return text2.Substring(text.Length); } if (string.Equals(text2, name, StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) { string text3 = args[i + 1] ?? string.Empty; if (!text3.StartsWith("-", StringComparison.Ordinal)) { return text3; } return string.Empty; } } return string.Empty; } private void OnApplicationQuit() { StopServices(); } private void OnDestroy() { StopServices(); if (Instance == this) { Instance = null; } } private void StopServices() { if (!_stopped) { _stopped = true; StopSession(); } } private void StopSession() { StopResponder(); _snapshot = QuerySnapshot.Empty; _sessionNetwork = null; _idle = false; _sessionActive = false; } private void StopResponder() { _responder?.Stop(); _responder = null; _responderPort = 0; } } internal sealed class QueryConfig { private readonly ConfigEntryInt _queryPort; private readonly ConfigEntryBool _publicPlayerNames; public int QueryPort => _queryPort.Value; public bool PublicPlayerNames => _publicPlayerNames.Value; public QueryConfig(ConfigEntryInt queryPort, ConfigEntryBool publicPlayerNames) { _queryPort = queryPort; _publicPlayerNames = publicPlayerNames; } } internal static class QueryGameAccess { private static readonly Lazy<FieldInfo> WorldField = new Lazy<FieldInfo>(() => AccessTools.Field(typeof(ZNet), "m_world") ?? throw new MissingFieldException(typeof(ZNet).FullName, "m_world")); private static readonly Lazy<FieldInfo> PeersField = new Lazy<FieldInfo>(() => AccessTools.Field(typeof(ZNet), "m_peers") ?? throw new MissingFieldException(typeof(ZNet).FullName, "m_peers")); private static readonly Lazy<FieldInfo> ServerNameField = new Lazy<FieldInfo>(() => AccessTools.Field(typeof(ZNet), "m_ServerName") ?? throw new MissingFieldException(typeof(ZNet).FullName, "m_ServerName")); public static World? GetWorld() { object? value = WorldField.Value.GetValue(null); return (World?)((value is World) ? value : null); } public static List<ZNetPeer> GetPeers(ZNet network) { return (PeersField.Value.GetValue(network) as List<ZNetPeer>) ?? throw new InvalidOperationException("ZNet.m_peers did not contain a peer list."); } public static string GetServerName() { return (ServerNameField.Value.GetValue(null) as string) ?? string.Empty; } } public sealed class QueryModule : IFeatureModule { private readonly FeatureDefinition _feature; private readonly QueryConfig _config; private readonly ModLogger _log; public string Name => "Server query"; public string Section => "Query"; public bool IsEnabled => _feature.Enabled.Value; public FeatureClassification Classification => FeatureClassification.ServerAuthoritative; public QueryModule(FeatureRegistry registry) { _log = new ModLogger(Logger.CreateLogSource("ValheimOne.Query")); _feature = registry.Register(Name, Section, Classification); ConfigEntryInt queryPort = _feature.Int("QueryPort", 0, "UDP port for the A2S query responder. 0 uses game port + 4. (Game port + 1 is Valheim's own Steam query port when crossplay is off; ValheimOne avoids it.)"); ConfigEntryBool publicPlayerNames = _feature.Bool("PublicPlayerNames", defaultValue: false, "Report real player names in A2S_PLAYER replies. When disabled, report generic slots."); _config = new QueryConfig(queryPort, publicPlayerNames); } public void ApplyPatches(Harmony harmony) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_0053: Expected O, but got Unknown if ((Object)(object)QueryBehaviour.Instance != (Object)null) { _log.Warning("[Query] behaviour already exists; skipping duplicate initialization."); return; } GameObject val = new GameObject("ValheimOne.Query") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); QueryBehaviour.Initialize(val, _config, _log, () => _feature.Enabled.Value); } } internal sealed class QueryResponder { private sealed class ChallengeEntry { public int Value { get; } public long IssuedTicks { get; } public long ExpiresTicks { get; } public ChallengeEntry(int value, long issuedTicks, long expiresTicks) { Value = value; IssuedTicks = issuedTicks; ExpiresTicks = expiresTicks; } } private sealed class PacketWriter : IDisposable { private readonly MemoryStream _stream = new MemoryStream(); public void WriteHeader(byte responseType) { WriteByte(byte.MaxValue); WriteByte(byte.MaxValue); WriteByte(byte.MaxValue); WriteByte(byte.MaxValue); WriteByte(responseType); } public void WriteByte(byte value) { _stream.WriteByte(value); } public void WriteInt16(short value) { ushort num = (ushort)value; WriteByte((byte)num); WriteByte((byte)(num >> 8)); } public void WriteInt32(int value) { WriteByte((byte)value); WriteByte((byte)((uint)value >> 8)); WriteByte((byte)((uint)value >> 16)); WriteByte((byte)((uint)value >> 24)); } public void WriteUInt64(ulong value) { for (int i = 0; i < 64; i += 8) { WriteByte((byte)(value >> i)); } } public void WriteSingle(float value) { byte[] bytes = BitConverter.GetBytes(value); if (!BitConverter.IsLittleEndian) { Array.Reverse((Array)bytes); } _stream.Write(bytes, 0, bytes.Length); } public void WriteString(string value) { string s = (value ?? string.Empty).Replace('\0', ' '); byte[] bytes = Encoding.UTF8.GetBytes(s); _stream.Write(bytes, 0, bytes.Length); WriteByte(0); } public byte[] ToArray() { return _stream.ToArray(); } public void Dispose() { _stream.Dispose(); } } private const int AppId = 892970; private const int MaximumDatagramSize = 1400; private const int MaximumChallenges = 256; private const long ChallengeLifetimeTicks = 300000000L; private static readonly byte EnvironmentByte = (byte)((Environment.OSVersion.Platform == PlatformID.Win32NT) ? 119 : 108); private static readonly byte[] RequestPrefix = new byte[4] { 255, 255, 255, 255 }; private static readonly byte[] InfoRequestName = Encoding.ASCII.GetBytes("Source Engine Query\0"); private readonly int _port; private readonly Func<QuerySnapshot> _snapshotProvider; private readonly ModLogger _log; private readonly RandomNumberGenerator _random = RandomNumberGenerator.Create(); private readonly Dictionary<IPEndPoint, ChallengeEntry> _challenges = new Dictionary<IPEndPoint, ChallengeEntry>(); private UdpClient? _socket; private Thread? _thread; private volatile bool _stopping; private bool _bindWarningLogged; private bool _workerWarningLogged; public bool IsRunning { get { if (_thread != null) { return _thread.IsAlive; } return false; } } public QueryResponder(int port, Func<QuerySnapshot> snapshotProvider, ModLogger log) { _port = port; _snapshotProvider = snapshotProvider; _log = log; } public bool Start() { if (IsRunning) { return true; } CloseSocket(); UdpClient socket; try { socket = new UdpClient(new IPEndPoint(IPAddress.Any, _port)); } catch (Exception ex) { if (!_bindWarningLogged) { _bindWarningLogged = true; _log.Warning($"[Query] A2S responder could not bind UDP {_port}: " + ex.GetType().Name + ": " + ex.Message); } return false; } _stopping = false; _workerWarningLogged = false; _socket = socket; _thread = new Thread((ThreadStart)delegate { Listen(socket); }) { IsBackground = true, Name = "ValheimOne.Query" }; _thread.Start(); _log.Info($"[Query] A2S responder listening on UDP {_port}"); return true; } public void Stop() { bool isRunning = IsRunning; _stopping = true; CloseSocket(); Thread thread = _thread; if (thread != null && thread.IsAlive && Thread.CurrentThread != thread) { thread.Join(); } _thread = null; _challenges.Clear(); _random.Dispose(); if (isRunning) { _log.Info($"[Query] A2S responder stopped on UDP {_port}"); } } private void Listen(UdpClient socket) { while (!_stopping) { try { IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0); byte[] datagram = socket.Receive(ref remoteEP); HandleDatagram(socket, remoteEP, datagram); } catch (ObjectDisposedException) { if (!_stopping) { LogWorkerFailure("UDP socket was disposed unexpectedly."); } } catch (SocketException ex2) { if (!_stopping) { LogWorkerFailure("SocketException: " + ex2.Message); } } catch (Exception ex3) { if (!_stopping) { LogWorkerFailure(ex3.GetType().Name + ": " + ex3.Message); } } } } private void HandleDatagram(UdpClient socket, IPEndPoint remoteEndpoint, byte[] datagram) { if (datagram.Length >= 5 && datagram.Length <= 1400 && HasPrefix(datagram)) { switch (datagram[4]) { case 84: HandleInfoRequest(socket, remoteEndpoint, datagram); break; case 85: HandlePlayerRequest(socket, remoteEndpoint, datagram); break; } } } private void HandleInfoRequest(UdpClient socket, IPEndPoint remoteEndpoint, byte[] datagram) { int num = 5 + InfoRequestName.Length; if ((datagram.Length != num && datagram.Length != num + 4) || !MatchesInfoRequestName(datagram)) { return; } if (datagram.Length == num || !HasValidChallenge(remoteEndpoint, ReadInt32(datagram, num))) { SendChallenge(socket, remoteEndpoint); return; } QuerySnapshot querySnapshot = _snapshotProvider(); using PacketWriter packetWriter = new PacketWriter(); packetWriter.WriteHeader(73); packetWriter.WriteByte(17); packetWriter.WriteString(querySnapshot.ServerName); packetWriter.WriteString(querySnapshot.WorldName); packetWriter.WriteString("valheim"); packetWriter.WriteString("Valheim"); packetWriter.WriteInt16(-24534); packetWriter.WriteByte(ClampByte(querySnapshot.PlayerCount)); packetWriter.WriteByte(ClampByte(querySnapshot.MaxPlayers)); packetWriter.WriteByte(0); packetWriter.WriteByte(100); packetWriter.WriteByte(EnvironmentByte); packetWriter.WriteByte(querySnapshot.Passworded ? ((byte)1) : ((byte)0)); packetWriter.WriteByte(0); packetWriter.WriteString(querySnapshot.GameVersion); packetWriter.WriteByte(161); packetWriter.WriteInt16((short)querySnapshot.GamePort); packetWriter.WriteString("valheimone,vo=" + querySnapshot.PluginVersion); packetWriter.WriteUInt64(892970uL); Send(socket, remoteEndpoint, packetWriter.ToArray()); } private void HandlePlayerRequest(UdpClient socket, IPEndPoint remoteEndpoint, byte[] datagram) { if (datagram.Length != 9) { return; } int num = ReadInt32(datagram, 5); if (num == -1 || !HasValidChallenge(remoteEndpoint, num)) { SendChallenge(socket, remoteEndpoint); return; } QuerySnapshot querySnapshot = _snapshotProvider(); int num2 = Math.Min(255, Math.Max(0, querySnapshot.PlayerCount)); double totalSeconds = (DateTime.UtcNow - querySnapshot.StartTimeUtc).TotalSeconds; float value = ((totalSeconds <= 0.0) ? 0f : ((totalSeconds >= 3.4028234663852886E+38) ? float.MaxValue : ((float)totalSeconds))); using PacketWriter packetWriter = new PacketWriter(); packetWriter.WriteHeader(68); packetWriter.WriteByte((byte)num2); for (int i = 0; i < num2; i++) { string value2 = querySnapshot.GetPlayerName(i); if (string.IsNullOrEmpty(value2)) { value2 = $"Player {i + 1}"; } packetWriter.WriteByte((byte)i); packetWriter.WriteString(value2); packetWriter.WriteInt32(0); packetWriter.WriteSingle(value); } Send(socket, remoteEndpoint, packetWriter.ToArray()); } private bool HasValidChallenge(IPEndPoint remoteEndpoint, int challenge) { long ticks = DateTime.UtcNow.Ticks; RemoveExpiredChallenges(ticks); if (_challenges.TryGetValue(remoteEndpoint, out ChallengeEntry value) && value.ExpiresTicks > ticks) { return value.Value == challenge; } return false; } private void SendChallenge(UdpClient socket, IPEndPoint remoteEndpoint) { int value = CreateChallenge(remoteEndpoint); using PacketWriter packetWriter = new PacketWriter(); packetWriter.WriteHeader(65); packetWriter.WriteInt32(value); Send(socket, remoteEndpoint, packetWriter.ToArray()); } private int CreateChallenge(IPEndPoint remoteEndpoint) { long ticks = DateTime.UtcNow.Ticks; RemoveExpiredChallenges(ticks); if (_challenges.Count >= 256) { IPEndPoint iPEndPoint = null; long num = long.MaxValue; foreach (KeyValuePair<IPEndPoint, ChallengeEntry> challenge in _challenges) { if (challenge.Value.IssuedTicks < num) { iPEndPoint = challenge.Key; num = challenge.Value.IssuedTicks; } } if (iPEndPoint != null) { _challenges.Remove(iPEndPoint); } } byte[] array = new byte[4]; int num2; do { _random.GetBytes(array); num2 = ReadInt32(array, 0); } while (num2 == 0 || num2 == -1); IPEndPoint key = new IPEndPoint(remoteEndpoint.Address, remoteEndpoint.Port); _challenges[key] = new ChallengeEntry(num2, ticks, ticks + 300000000); return num2; } private void RemoveExpiredChallenges(long nowTicks) { if (_challenges.Count == 0) { return; } List<IPEndPoint> list = new List<IPEndPoint>(); foreach (KeyValuePair<IPEndPoint, ChallengeEntry> challenge in _challenges) { if (challenge.Value.ExpiresTicks <= nowTicks) { list.Add(challenge.Key); } } for (int i = 0; i < list.Count; i++) { _challenges.Remove(list[i]); } } private static bool HasPrefix(byte[] datagram) { for (int i = 0; i < RequestPrefix.Length; i++) { if (datagram[i] != RequestPrefix[i]) { return false; } } return true; } private static bool MatchesInfoRequestName(byte[] datagram) { for (int i = 0; i < InfoRequestName.Length; i++) { if (datagram[i + 5] != InfoRequestName[i]) { return false; } } return true; } private static int ReadInt32(byte[] bytes, int offset) { return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24); } private static byte ClampByte(int value) { return (byte)Math.Min(255, Math.Max(0, value)); } private static void Send(UdpClient socket, IPEndPoint remoteEndpoint, byte[] response) { socket.Send(response, response.Length, remoteEndpoint); } private void CloseSocket() { UdpClient socket = _socket; _socket = null; if (socket == null) { return; } try { socket.Close(); } catch (ObjectDisposedException) { } } private void LogWorkerFailure(string message) { if (!_workerWarningLogged) { _workerWarningLogged = true; _log.Warning("[Query] A2S responder worker error: " + message); } } } internal sealed class QuerySnapshot { private readonly string[] _playerNames; public static readonly QuerySnapshot Empty = new QuerySnapshot(string.Empty, string.Empty, 0, Array.Empty<string>(), 0, 2456, passworded: false, "unknown", "0.13.29", DateTime.UtcNow); public string ServerName { get; } public string WorldName { get; } public int PlayerCount { get; } public int MaxPlayers { get; } public int GamePort { get; } public bool Passworded { get; } public string GameVersion { get; } public string PluginVersion { get; } public DateTime StartTimeUtc { get; } public QuerySnapshot(string serverName, string worldName, int playerCount, string[] playerNames, int maxPlayers, int gamePort, bool passworded, string gameVersion, string pluginVersion, DateTime startTimeUtc) { ServerName = serverName; WorldName = worldName; PlayerCount = playerCount; _playerNames = (string[])playerNames.Clone(); MaxPlayers = maxPlayers; GamePort = gamePort; Passworded = passworded; GameVersion = gameVersion; PluginVersion = pluginVersion; StartTimeUtc = startTimeUtc; } public string GetPlayerName(int index) { if (index < 0 || index >= _playerNames.Length) { return string.Empty; } return _playerNames[index]; } } } namespace ValheimOne.Networking { internal sealed class AckGatedChunkQueue<T> where T : class, IAcknowledgedChunk { private readonly Queue<T> _pending = new Queue<T>(); public bool IsAwaitingAcknowledgement { get; private set; } public int AwaitingIndex { get; private set; } = -1; public int AcknowledgedCount { get; private set; } public int PendingCount => _pending.Count; public bool IsIdle { get { if (!IsAwaitingAcknowledgement) { return _pending.Count == 0; } return false; } } public void Enqueue(T chunk) { _pending.Enqueue(chunk); } public bool TryStartNext(out T? chunk) { if (IsAwaitingAcknowledgement || _pending.Count == 0) { chunk = null; return false; } chunk = _pending.Dequeue(); IsAwaitingAcknowledgement = true; AwaitingIndex = chunk.Index; return true; } public bool TryAcknowledge(int index) { if (!IsAwaitingAcknowledgement || AwaitingIndex != index) { return false; } IsAwaitingAcknowledgement = false; AwaitingIndex = -1; AcknowledgedCount++; return true; } public void Reset() { _pending.Clear(); IsAwaitingAcknowledgement = false; AwaitingIndex = -1; AcknowledgedCount = 0; } } internal static class ConfigSyncSerializer { public const int MaximumChunkTextBytes = 4000; public static string Serialize(FeatureRegistry registry) { StringBuilder stringBuilder = new StringBuilder(); foreach (FeatureDefinition feature in registry.Features) { if (feature.Classification == FeatureClassification.ClientOnly || feature.Classification == FeatureClassification.ServerOnly) { continue; } foreach (IConfigEntry key in feature.Keys) { if (!key.Definition.IsSensitive) { stringBuilder.Append('[').Append(feature.Section).Append("] / ") .Append(key.Definition.Name) .Append('=') .Append(key.GetSerializedValue()) .Append('\n'); } } } return stringBuilder.ToString(); } public static string ComputeHash(string serializedConfig) { byte[] bytes = Encoding.UTF8.GetBytes(serializedConfig); byte[] array; using (SHA256 sHA = SHA256.Create()) { array = sHA.ComputeHash(bytes); } StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } public static IReadOnlyList<string> CreateChunks(string serializedConfig) { List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); int num = 0; using StringReader stringReader = new StringReader(serializedConfig); string text; while ((text = stringReader.ReadLine()) != null) { string text2 = text + "\n"; int byteCount = Encoding.UTF8.GetByteCount(text2); if (byteCount > 4000) { throw new InvalidOperationException($"A serialized config line exceeds the {4000}-byte chunk limit."); } if (stringBuilder.Length != 0 && num + byteCount > 4000) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); num = 0; } stringBuilder.Append(text2); num += byteCount; } if (stringBuilder.Length != 0 || list.Count == 0) { list.Add(stringBuilder.ToString()); } return list; } } internal readonly struct ExploredMapRange { public int StartX { get; } public int EndX { get; } public int Y { get; } public ExploredMapRange(int startX, int endX, int y) { StartX = startX; EndX = endX; Y = y; } } internal interface IAcknowledgedChunk { int Index { get; } } public interface IVersionHandshake { bool IsAvailable { get; } void Initialize(Harmony harmony); void Shutdown(); } public interface IVersionHandshakeExtension { void RegisterRpcHandlers(ZRoutedRpc routedRpc); void OnPeerCompatible(long peerId); void Pump(ZNet net, ZRoutedRpc routedRpc, IReadOnlyCollection<long> compatiblePeerIds); bool TryHandleAcknowledgement(long sender, string channel, int index); void ResetNetworkState(); void Shutdown(); } public sealed class VersionHandshake : IVersionHandshake { private sealed class PeerHandshakeState { public ZNetPeer Peer { get; } public long PeerId { get; } public float FirstObservedAt { get; } public bool HelloReceived { get; set; } public bool Compatible { get; set; } public bool VanillaClassified { get; set; } public bool KickIssued { get; set; } public string RemoteVersion { get; set; } = "unknown"; public int RemoteSchema { get; set; } = -1; public string RemoteConfigHash { get; set; } = string.Empty; public AckGatedChunkQueue<OutboundConfigChunk> ConfigQueue { get; } = new AckGatedChunkQueue<OutboundConfigChunk>(); public PeerHandshakeState(ZNetPeer peer, float firstObservedAt) { Peer = peer; PeerId = peer.m_uid; FirstObservedAt = firstObservedAt; } public void ResetConfigPush() { ConfigQueue.Reset(); } } private sealed class ClientConfigBuffer { public int TotalChunks { get; } public string?[] Chunks { get; } public int ReceivedChunks { get; set; } public ClientConfigBuffer(int totalChunks) { TotalChunks = totalChunks; Chunks = new string[totalChunks]; } } private sealed class OutboundConfigChunk : IAcknowledgedChunk { public int TotalChunks { get; } public int Index { get; } public string Text { get; } public OutboundConfigChunk(int totalChunks, int index, string text) { TotalChunks = totalChunks; Index = index; Text = text; } } private const string HelloRpc = "VO_Hello"; private const string ConfigRpc = "VO_Config"; internal const string AckRpc = "VO_Ack"; private const int MaximumConfigChunks = 1024; private static VersionHandshake? _active; private readonly ValheimOneConfig _settings; private readonly ServerConfig _serverConfig; private readonly ModLogger _log; private readonly IReadOnlyList<IVersionHandshakeExtension> _extensions; private readonly Dictionary<long, PeerHandshakeState> _peerStates = new Dictionary<long, PeerHandshakeState>(); private readonly List<ZRoutedRpc> _registeredRpcInstances = new List<ZRoutedRpc>(); private readonly List<ZRoutedRpc> _failedRpcInstances = new List<ZRoutedRpc>(); private ClientConfigBuffer? _clientConfigBuffer; private bool _clientHelloFailureLogged; private bool _clientHelloSent; private bool _initialized; public bool IsAvailable { get { if (_active == this) { return _registeredRpcInstances.Count != 0; } return false; } } private bool IsEnforcementEnabled { get { if (_serverConfig.Enabled) { return _serverConfig.EnforceMod.Value; } return false; } } public VersionHandshake(ValheimOneConfig settings, ServerConfig serverConfig, ModLogger log) : this(settings, serverConfig, log, Array.Empty<IVersionHandshakeExtension>()) { } internal VersionHandshake(ValheimOneConfig settings, ServerConfig serverConfig, ModLogger log, IReadOnlyList<IVersionHandshakeExtension> extensions) { _settings = settings; _serverConfig = serverConfig; _log = log; _extensions = extensions; } public void Initialize(Harmony harmony) { if (!_initialized) { _active = this; PatchPostfix(harmony, typeof(Game), "Start", Type.EmptyTypes, "GameStartPostfix"); PatchPostfix(harmony, typeof(ZNet), "RPC_PeerInfo", new Type[2] { typeof(ZRpc), typeof(ZPackage) }, "PeerInfoPostfix"); PatchPostfix(harmony, typeof(ZNet), "SendPeriodicData", new Type[1] { typeof(float) }, "SendPeriodicDataPostfix"); PatchPostfix(harmony, typeof(ZNet), "OnDestroy", Type.EmptyTypes, "ZNetOnDestroyPostfix"); _initialized = true; _log.Info("Server enforcement chassis ready (VO_Hello, VO_Config, VO_Ack)."); } } public void Shutdown() { if (!_initialized) { return; } if (_active == this) { _active = null; } foreach (IVersionHandshakeExtension extension in _extensions) { extension.Shutdown(); } _settings.ClearOverlay(); _peerStates.Clear(); _clientConfigBuffer = null; _clientHelloFailureLogged = false; _clientHelloSent = false; _registeredRpcInstances.Clear(); _failedRpcInstances.Clear(); _initialized = false; } private static void PatchPostfix(Harmony harmony, Type declaringType, string methodName, Type[] parameterTypes, string postfixName) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(declaringType, methodName, parameterTypes, (Type[])null) ?? throw new MissingMethodException(declaringType.FullName, methodName); HarmonyMethod val = new HarmonyMethod(typeof(VersionHandshake), postfixName, (Type[])null); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void GameStartPostfix() { VersionHandshake active = _active; if (active != null) { active.RegisterRpcHandlers(); ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && !instance.IsServer()) { active.TrySendClientHello(instance); } } } private static void PeerInfoPostfix(ZNet __instance) { if (!__instance.IsServer()) { _active?.TrySendClientHello(__instance); } } private static void SendPeriodicDataPostfix(ZNet __instance) { VersionHandshake active = _active; if (active != null) { active.RegisterRpcHandlers(); if (__instance.IsServer()) { active.PumpServer(__instance); } else if (!active._clientHelloSent) { active.TrySendClientHello(__instance); } active.PumpExtensions(__instance); } } private static void ZNetOnDestroyPostfix() { _active?.ResetNetworkState(); } private void RegisterRpcHandlers() { if (_active != this) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || IsRegistered(instance) || IsRegistrationFailed(instance)) { return; } try { instance.Register<ZPackage>("VO_Hello", (Action<long, ZPackage>)HandleHello); instance.Register<ZPackage>("VO_Config", (Action<long, ZPackage>)HandleConfig); instance.Register<ZPackage>("VO_Ack", (Action<long, ZPackage>)HandleAck); foreach (IVersionHandshakeExtension extension in _extensions) { extension.RegisterRpcHandlers(instance); } _registeredRpcInstances.Add(instance); _log.Debug("Registered ValheimOne routed RPC handlers."); } catch (Exception ex) { _failedRpcInstances.Add(instance); _log.Error("Unable to register ValheimOne routed RPCs: " + ex.Message); } } private bool IsRegistered(ZRoutedRpc routedRpc) { foreach (ZRoutedRpc registeredRpcInstance in _registeredRpcInstances) { if (registeredRpcInstance == routedRpc) { return true; } } return false; } private bool IsRegistrationFailed(ZRoutedRpc routedRpc) { foreach (ZRoutedRpc failedRpcInstance in _failedRpcInstances) { if (failedRpcInstance == routedRpc) { return true; } } return false; } private void TrySendClientHello(ZNet net) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown if (_clientHelloSent || net.IsServer()) { return; } RegisterRpcHandlers(); ZRoutedRpc instance = ZRoutedRpc.instance; ZNetPeer serverPeer = net.GetServerPeer(); if (instance == null || !IsRegistered(instance) || serverPeer == null || !serverPeer.IsReady()) { return; } long uid = serverPeer.m_uid; if (uid == 0L) { return; } try { string serializedConfig = ConfigSyncSerializer.Serialize(_settings.Features); ZPackage val = new ZPackage(); val.Write("0.13.29"); val.Write(1); val.Write(ConfigSyncSerializer.ComputeHash(serializedConfig)); instance.InvokeRoutedRPC(uid, "VO_Hello", new object[1] { val }); _clientHelloSent = true; _log.Info("Sent VO_Hello to server (ValheimOne v0.13.29, " + $"schema {1})."); } catch (Exception ex) { if (!_clientHelloFailureLogged) { _clientHelloFailureLogged = true; _log.Warning("Unable to send VO_Hello to the server: " + ex.Message); } } } private void HandleHello(long sender, ZPackage package) { ZNet instance = ZNet.instance; if (_active != this || (Object)(object)instance == (Object)null || !instance.IsServer()) { return; } ZNetPeer peer = instance.GetPeer(sender); if (peer == null || peer.m_server) { _log.Warning(string.Format("Ignored {0} from unknown peer uid {1}.", "VO_Hello", sender)); return; } string text; int num; string text2; try { text = package.ReadString(); num = package.ReadInt(); text2 = package.ReadString(); } catch (Exception ex) { HandleMalformedHello(instance, peer, ex.Message); return; } if (text.Length > 64 || text2.Length > 128) { HandleMalformedHello(instance, peer, "version or config hash exceeds the protocol limit"); return; } PeerHandshakeState orCreateState = GetOrCreateState(peer); if (orCreateState.HelloReceived) { return; } orCreateState.HelloReceived = true; orCreateState.RemoteVersion = text; orCreateState.RemoteSchema = num; orCreateState.RemoteConfigHash = text2; orCreateState.Compatible = VersionInfo.IsCompatible(text, num); if (!orCreateState.Compatible) { string text3 = $"incompatible ValheimOne handshake (client v{text}, schema {num}; " + string.Format("server v{0}, schema {1})", "0.13.29", 1); if (IsEnforcementEnabled) { KickPeer(instance, orCreateState, text3); return; } _log.Info("Peer " + PeerLabel(peer) + " is modded but " + text3 + "; allowed because Server.EnforceMod=false."); return; } string serializedConfig; string a; try { serializedConfig = ConfigSyncSerializer.Serialize(_settings.Features); a = ConfigSyncSerializer.ComputeHash(serializedConfig); } catch (Exception ex2) { _log.Error("Handshake with peer " + PeerLabel(peer) + " succeeded, but server config serialization failed: " + ex2.Message); return; } string arg = (string.Equals(a, text2, StringComparison.OrdinalIgnoreCase) ? "config already matches" : "server config will win"); _log.Info("Peer " + PeerLabel(peer) + " is modded: handshake ok " + $"(ValheimOne v{text}, schema {num}; {arg})."); foreach (IVersionHandshakeExtension extension in _extensions) { extension.OnPeerCompatible(sender); } if (_serverConfig.Enabled && _serverConfig.SyncConfig.Value) { QueueConfigPush(orCreateState, serializedConfig); } } private void HandleMalformedHello(ZNet net, ZNetPeer peer, string detail) { PeerHandshakeState orCreateState = GetOrCreateState(peer); orCreateState.HelloReceived = true; orCreateState.Compatible = false; string text = "malformed ValheimOne handshake (" + detail + ")"; if (IsEnforcementEnabled) { KickPeer(net, orCreateState, text); return; } _log.Info("Peer " + PeerLabel(peer) + " sent a " + text + "; allowed because Server.EnforceMod=false."); } private void QueueConfigPush(PeerHandshakeState state, string serializedConfig) { IReadOnlyList<string> readOnlyList; try { readOnlyList = ConfigSyncSerializer.CreateChunks(serializedConfig); } catch (Exception ex) { _log.Error("Unable to prepare config push for peer " + PeerLabel(state.Peer) + ": " + ex.Message); return; } state.ResetConfigPush(); for (int i = 0; i < readOnlyList.Count; i++) { state.ConfigQueue.Enqueue(new OutboundConfigChunk(readOnlyList.Count, i, readOnlyList[i])); } _log.Info($"Config push to peer {PeerLabel(state.Peer)} queued: {readOnlyList.Count} chunk(s)."); } private void HandleConfig(long sender, ZPackage package) { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (_active != this || (Object)(object)instance == (Object)null || instance2 == null || instance.IsServer()) { return; } long num = instance.GetServerPeer()?.m_uid ?? 0; if (sender != num) { _log.Warning(string.Format("Ignored spoofed {0} from peer uid {1}.", "VO_Config", sender)); return; } int num2; int num3; string text; try { num2 = package.ReadInt(); num3 = package.ReadInt(); text = package.ReadString(); } catch (Exception ex) { _log.Warning("Ignored malformed VO_Config from the server: " + ex.Message); return; } if (num2 <= 0 || num2 > 1024 || num3 < 0 || num3 >= num2) { _log.Warning(string.Format("Ignored invalid {0} chunk header ({1}/{2}).", "VO_Config", num3 + 1, num2)); return; } if (Encoding.UTF8.GetByteCount(text) > 4000) { _log.Warning(string.Format("Ignored oversized {0} chunk {1}/{2}.", "VO_Config", num3 + 1, num2)); return; } ClientConfigBuffer clientConfigBuffer = _clientConfigBuffer; if (clientConfigBuffer == null) { if (num3 != 0) { _log.Warning(string.Format("Ignored out-of-sequence {0} chunk {1}/{2}.", "VO_Config", num3 + 1, num2)); return; } clientConfigBuffer = (_clientConfigBuffer = new ClientConfigBuffer(num2)); } if (clientConfigBuffer.TotalChunks != num2) { _log.Warning("Ignored config chunk whose total does not match the active push."); return; } string text2 = clientConfigBuffer.Chunks[num3]; if (text2 != null && !string.Equals(text2, text, StringComparison.Ordinal)) { _log.Warning(string.Format("Ignored conflicting duplicate {0} chunk {1}/{2}.", "VO_Config", num3 + 1, num2)); return; } if (text2 == null) { if (num3 != clientConfigBuffer.ReceivedChunks) { _log.Warning(string.Format("Ignored out-of-sequence {0} chunk {1}/{2}.", "VO_Config", num3 + 1, num2)); return; } clientConfigBuffer.Chunks[num3] = text; clientConfigBuffer.ReceivedChunks++; } SendAck(instance2, sender, num3); if (clientConfigBuffer.ReceivedChunks != clientConfigBuffer.TotalChunks) { return; } _clientConfigBuffer = null; StringBuilder stringBuilder = new StringBuilder(); string[] chunks = clientConfigBuffer.Chunks; foreach (string value in chunks) { stringBuilder.Append(value); } try { int num4 = _settings.ApplyOverlay(stringBuilder.ToString()); _log.Info($"Server config applied: {num4} value(s) from " + $"{clientConfigBuffer.TotalChunks} chunk(s); local cfg unchanged."); } catch (Exception ex2) { _log.Error("Server config overlay rejected; local settings remain active: " + ex2.Message); } } private void SendAck(ZRoutedRpc routedRpc, long serverPeerId, int index) { //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(); val.Write("VO_Config"); val.Write(index); try { routedRpc.InvokeRoutedRPC(serverPeerId, "VO_Ack", new object[1] { val }); } catch (Exception ex) { _log.Warning($"Unable to acknowledge config chunk {index + 1}: {ex.Message}"); } } private void HandleAck(long sender, ZPackage package) { ZNet instance = ZNet.instance; if (_active != this || (Object)(object)instance == (Object)null) { return; } string text; int num; try { text = package.ReadString(); num = package.ReadInt(); } catch (Exception ex) { _log.Warning(string.Format("Ignored malformed {0} from peer uid {1}: {2}", "VO_Ack", sender, ex.Message)); return; } PeerHandshakeState value; if (!string.Equals(text, "VO_Config", StringComparison.Ordinal)) { foreach (IVersionHandshakeExtension extension in _extensions) { if (extension.TryHandleAcknowledgement(sender, text, num)) { return; } } _log.Warning(string.Format("Ignored unexpected {0} for {1} chunk {2} from peer uid {3}.", "VO_Ack", text, num + 1, sender)); } else if (!instance.IsServer() || !_peerStates.TryGetValue(sender, out value) || !value.ConfigQueue.TryAcknowledge(num)) { _log.Warning(string.Format("Ignored unexpected {0} for config chunk {1} from peer uid {2}.", "VO_Ack", num + 1, sender)); } else if (value.ConfigQueue.IsIdle) { _log.Info("Config push to peer " + PeerLabel(value.Peer) + " complete: " + $"{value.ConfigQueue.AcknowledgedCount} chunk(s) sent and acknowledged."); value.ResetConfigPush(); } } private void PumpServer(ZNet net) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected O, but got Unknown ReconcileServerPeers(net); if (!_serverConfig.Enabled) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || !IsRegistered(instance)) { return; } foreach (PeerHandshakeState item in new List<PeerHandshakeState>(_peerStates.Values)) { if (!item.KickIssued && item.Compatible && item.ConfigQueue.TryStartNext(out OutboundConfigChunk chunk) && chunk != null) { ZPackage val = new ZPackage(); val.Write(chunk.TotalChunks); val.Write(chunk.Index); val.Write(chunk.Text); try { instance.InvokeRoutedRPC(item.PeerId, "VO_Config", new object[1] { val }); _log.Debug("Config push to peer " + PeerLabel(item.Peer) + " sent chunk " + $"{chunk.Index + 1}/{chunk.TotalChunks}."); } catch (Exception ex) { item.ResetConfigPush(); _log.Error("Config push to peer " + PeerLabel(item.Peer) + " failed: " + ex.Message); } } } } private void ReconcileServerPeers(ZNet net) { float realtimeSinceStartup = Time.realtimeSinceStartup; HashSet<long> hashSet = new HashSet<long>(); foreach (ZNetPeer item in new List<ZNetPeer>(net.GetPeers())) { if (item.m_server || !item.IsReady()) { continue; } hashSet.Add(item.m_uid); PeerHandshakeState orCreateState = GetOrCreateState(item, realtimeSinceStartup); if (orCreateState.KickIssued) { continue; } if (orCreateState.HelloReceived && !orCreateState.Compatible) { if (IsEnforcementEnabled) { KickPeer(net, orCreateState, "incompatible ValheimOne handshake (client v" + orCreateState.RemoteVersion + ", " + $"schema {orCreateState.RemoteSchema})"); } } else { if (orCreateState.HelloReceived) { continue; } int num = Math.Max(0, _serverConfig.HandshakeGraceSeconds.Value); if (realtimeSinceStartup - orCreateState.FirstObservedAt < (float)num) { continue; } if (orCreateState.VanillaClassified) { if (IsEnforcementEnabled) { KickPeer(net, orCreateState, string.Format("no {0} received within {1}s (vanilla client)", "VO_Hello", num)); } } else if (_serverConfig.Enabled) { orCreateState.VanillaClassified = true; string text = string.Format("no {0} received within {1}s (vanilla client)", "VO_Hello", num); if (IsEnforcementEnabled) { KickPeer(net, orCreateState, text); } else if (_serverConfig.Enabled) { _log.Info("Peer " + PeerLabel(item) + " is vanilla: " + text + "; allowed because Server.EnforceMod=false."); } } } } List<long> list = new List<long>(); foreach (long key in _peerStates.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (long item2 in list) { _peerStates.Remove(item2); } } private PeerHandshakeState GetOrCreateState(ZNetPeer peer) { return GetOrCreateState(peer, Time.realtimeSinceStartup); } private PeerHandshakeState GetOrCreateState(ZNetPeer peer, float observedAt) { if (_peerStates.TryGetValue(peer.m_uid, out PeerHandshakeState value) && value.Peer == peer) { return value; } value = new PeerHandshakeState(peer, observedAt); _peerStates[peer.m_uid] = value; return value; } private void KickPeer(ZNet net, PeerHandshakeState state, string reason) { if (state.KickIssued) { return; } state.KickIssued = true; state.ResetConfigPush(); _log.Warning("Peer " + PeerLabel(state.Peer) + " kicked: " + reason + "."); try { net.Kick(state.Peer.m_playerName); } catch (Exception ex) { _log.Error("Failed to kick peer " + PeerLabel(state.Peer) + " after handshake rejection: " + ex.Message); } } private void ResetNetworkState() { foreach (IVersionHandshakeExtension extension in _extensions) { extension.ResetNetworkState(); } if (_settings.ClearOverlay()) { _log.Info("Server config overlay cleared; local settings restored."); } _peerStates.Clear(); _clientConfigBuffer = null; _clientHelloFailureLogged = false; _clientHelloSent = false; } private void PumpExtensions(ZNet net) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || !IsRegistered(instance)) { return; } List<long> list = new List<long>(); if (net.IsServer()) { foreach (PeerHandshakeState value in _peerStates.Values) { if (value.Compatible && !value.KickIssued && value.Peer.IsReady()) { list.Add(value.PeerId); } } } foreach (IVersionHandshakeExtension extension in _extensions) { extension.Pump(net, instance, list); } } private static string PeerLabel(ZNetPeer peer) { if (!string.IsNullOrWhiteSpace(peer.m_playerName)) { return $"'{peer.m_playerName}' (uid {peer.m_uid})"; } return $"uid {peer.m_uid}"; } } public static class VersionInfo { public const string PluginVersion = "0.13.29"; public const string SupportedGameVersion = "1.0.21"; public const int NetworkConfigSchema = 1; public static bool IsCompatible(string remotePluginVersion, int remoteSchema) { if (remoteSchema != 1 || !Version.TryParse("0.13.29", out Version result) || !Version.TryParse(remotePluginVersion, out Version result2)) { return false; } if (result.Major == result2.Major) { return result.Minor == result2.Minor; } return false; } } } namespace ValheimOne.Modules { public sealed class BeehiveModule : IFeatureModule { private sealed class BeehiveBaseline { public WeakReference<Beehive> Beehive { get; } public float SecondsPerUnit { get; } public int MaxHoney { get; } public BeehiveBaseline(Beehive beehive) { Beehive = new WeakReference<Beehive>(beehive); SecondsPerUnit = beehive.m_secPerUnit; MaxHoney = beehive.m_maxHoney; } public void Restore(Beehive beehive) { beehive.m_secPerUnit = SecondsPerUnit; beehive.m_maxHoney = MaxHoney; } } private static BeehiveModule? _active; private readonly FeatureDefinition _feature; private readonly ConfigEntryInt _productionSeconds; private readonly ConfigEntryInt _maxHoney; private readonly List<BeehiveBaseline> _baselines = new List<BeehiveBaseline>(); public string Name => "Beehive"; public string Section => "Beehive"; public bool IsEnabled => _feature.Enabled.Value; public FeatureClassification Classification => FeatureClassification.Synced; public BeehiveModule(FeatureRegistry registry) { _feature = registry.Register(Name, Section, Classification); _productionSeconds = _feature.Int("ProductionSeconds", 0, "Absolute seconds required to produce one honey. Zero disables this override; Valheim's default is 1200."); _maxHoney = _feature.Int("MaxHoney", 0, "Absolute maximum stored honey. Zero disables this override; Valheim's default is 4."); registry.EffectiveValuesChanged += OnEffectiveValuesChanged; } public void ApplyPatches(Harmony harmony) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown _active = this; MethodInfo methodInfo = AccessTools.Method(typeof(Beehive), "Awake", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("Beehive", "Awake"); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(BeehiveModule), "BeehiveAwakePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void BeehiveAwakePostfix(Beehive __instance) { BeehiveModule active = _active; if (active != null && active.IsEnabled) { active.ApplyToBeehive(__instance, active.GetOrAddBaseline(__instance)); } } private void OnEffectiveValuesChanged() { Beehive[] array = Object.FindObjectsOfType<Beehive>(); foreach (Beehive beehive in array) { BeehiveBaseline beehiveBaseline = FindBaseline(beehive); if (IsEnabled) { ApplyToBeehive(beehive, beehiveBaseline ?? AddBaseline(beehive)); } else { beehiveBaseline?.Restore(beehive); } } } private BeehiveBaseline GetOrAddBaseline(Beehive beehive) { return FindBaseline(beehive) ?? AddBaseline(beehive); } private BeehiveBaseline? FindBaseline(Beehive beehive) { for (int num = _baselines.Count - 1; num >= 0; num--) { BeehiveBaseline beehiveBaseline = _baselines[num]; if (!beehiveBaseline.Beehive.TryGetTarget(out Beehive target) || (Object)(object)target == (Object)null) { _baselines.RemoveAt(num); } else if (target == beehive) { return beehiveBaseline; } } return null; } private BeehiveBaseline AddBaseline(Beehive beehive) { BeehiveBaseline beehiveBaseline = new BeehiveBaseline(beehive); _baselines.Add(beehiveBaseline); return beehiveBaseline; } private void ApplyToBeehive(Beehive beehive, BeehiveBaseline baseline) { int value = _productionSeconds.Value; beehive.m_secPerUnit = ((value > 0) ? ((float)value) : baseline.SecondsPerUnit); int value2 = _maxHoney.Value; beehive.m_maxHoney = ((value2 > 0) ? value2 : baseline.MaxHoney); } } public sealed class BuildingQoLModule : IFeatureModule { private sealed class PlayerBuildingBaseline { public WeakReference<Player> Player { get; } public float MaxPlacementDistance { get; } public float PlaceRotationDegrees { get; } public PlayerBuildingBaseline(Player player) { Player = new WeakReference<Player>(player); MaxPlacementDistance = player.m_maxPlaceDistance; PlaceRotationDegrees = PlaceRotationDegreesField.Invoke(player); } public void Restore(Player player) { player.m_maxPlaceDistance = MaxPlacementDistance; PlaceRotationDegreesField.Invoke(player) = PlaceRotationDegrees; } } private static readonly FieldRef<Player, PlacementStatus> PlacementStatusField = AccessTools.FieldRefAccess<Player, PlacementStatus>("m_placementStatus"); private static readonly FieldRef<Player, GameObject> PlacementGhostField = AccessTools.FieldRefAccess<Player, GameObject>("m_placementGhost"); private static readonly FieldRef<Player, int> PlaceRotationField = AccessTools.FieldRefAccess<Player, int>("m_placeRotation"); private static readonly FieldRef<Player, float> PlaceRotationDegreesField = AccessTools.FieldRefAccess<Player, float>("m_placeRotationDegrees"); private static BuildingQoLModule? _active; private readonly FeatureDefinition _feature; private readonly ConfigEntryBool _noStructuralSupport; private readonly ConfigEntryBool _noPlacementBlocking; private readonly ConfigEntryFloat _maxPlacementDistance; private readonly ConfigEntryFloat _freeRotationStepDegrees; private readonly List<PlayerBuildingBaseline> _playerBaselines = new List<PlayerBuildingBaseline>(); public string Name => "Building quality of life"; public string Section => "Building"; public bool IsEnabled => _feature.Enabled.Value; public FeatureClassification Classification => FeatureClassification.Synced; public BuildingQoLModule(FeatureRegistry registry) { _feature = registry.Register(Name, Section, Classification); _noStructuralSupport = _feature.Bool("NoStructuralSupport", defaultValue: false, "Treat every structural-support check as supported, allowing pieces to remain in free air."); _noPlacementBlocking = _feature.Bool("NoPlacementBlocking", defaultValue: false, "Allow overlapping and otherwise invalid placement. No-build zones (mystical forces), wards, teleport-only areas, and other location or progression restrictions remain enforced."); _maxPlacementDistance = _feature.Float("MaxPlacementDistance", 0f, "Absolute hammer build reach. Zero disables this override; Valheim's default is 8."); _freeRotationStepDegrees = _feature.Float("FreeRotationStepDegrees", 0f, "Absolute placement rotation step in degrees. Zero disables this override; Valheim's default is 22.5."); registry.EffectiveValuesChanged += OnEffectiveValuesChanged; } public void ApplyPatches(Harmony harmony) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Expected O, but got Unknown _active = this; MethodInfo methodInfo = AccessTools.Method(typeof(WearNTear), "HaveSupport", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("WearNTear", "HaveSupport"); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(BuildingQoLModule), "HaveSupportPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo2 = AccessTools.Method(typeof(Player), "Awake", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("Player", "Awake"); harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(BuildingQoLModule), "PlayerAwakePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo3 = AccessTools.Method(typeof(Player), "UpdatePlacementGhost", new Type[1] { typeof(bool) }, (Type[])null) ?? throw new MissingMethodException("Player", "UpdatePlacementGhost"); harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(BuildingQoLModule), "UpdatePlacementGhostPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void HaveSupportPostfix(ref bool __result) { BuildingQoLModule active = _active; if (active != null && active.IsEnabled && active._noStructuralSupport.Value) { __result = true; } } private static void PlayerAwakePostfix(Player __instance) { BuildingQoLModule active = _active; if (active != null) { PlayerBuildingBaseline orAddBaseline = active.GetOrAddBaseline(__instance); if (active.IsEnabled) { active.ApplyToPlayer(__instance, orAddBaseline); } } } private static void UpdatePlacementGhostPostfix(Player __instance) { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) BuildingQoLModule active = _active; if (active == null || !active.IsEnabled) { return; } GameObject val = PlacementGhostField.Invoke(__instance); if (active._noPlacementBlocking.Value && IsSuppressiblePlacementStatus(PlacementStatusField.Invoke(__instance))) { PlacementStatusField.Invoke(__instance) = (PlacementStatus)0; Piece obj = (((Object)(object)val == (Object)null) ? null : val.GetComponent<Piece>()); if (obj != null) { obj.SetInvalidPlacementHeightlight(false); } } float value = active._freeRotationStepDegrees.Value; if (!(value <= 0f) && !((Object)(object)val == (Object)null)) { PlaceRotationDegreesField.Invoke(__instance) = value; val.transform.rotation = Quaternion.Euler(0f, value * (float)PlaceRotationField.Invoke(__instance), 0f); } } private static bool IsSuppressiblePlacementStatus(PlacementStatus status) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Invalid comparison between Unknown and I4 //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 if (status - 1 <= 1 || (int)status == 5) { return true; } return false; } private void OnEffectiveValuesChanged() { for (int num = _playerBaselines.Count - 1; num >= 0; num--) { PlayerBuildingBaseline playerBuildingBaseline = _playerBaselines[num]; if (!playerBuildingBaseline.Player.TryGetTarget(out Player target) || (Object)(object)target == (Object)null) { _playerBaselines.RemoveAt(num); } else if (IsEnabled) { ApplyToPlayer(target, playerBuildingBaseline); } else { playerBuildingBaseline.Restore(target); } } } private PlayerBuildingBaseline GetOrAddBaseline(Player player) { for (int num = _playerBaselines.Count - 1; num >= 0; num--) { PlayerBuildingBaseline playerBuildingBaseline = _playerBaselines[num]; if (!playerBuildingBaseline.Player.TryGetTarget(out Player target) || (Object)(object)target == (Object)null) { _playerBaselines.RemoveAt(num); } else if (target == player) { return playerBuildingBaseline; } } PlayerBuildingBaseline playerBuildingBaseline2 = new PlayerBuildingBaseline(player); _playerBaselines.Add(playerBuildingBaseline2); return playerBuildingBaseline2; } private void ApplyToPlayer(Player player, PlayerBuildingBaseline baseline) { float value = _maxPlacementDistance.Value; player.m_maxPlaceDistance = ((value > 0f) ? value : baseline.MaxPlacementDistance); float value2 = _freeRotationStepDegrees.Value; PlaceRotationDegreesField.Invoke(player) = ((value2 > 0f) ? value2 : baseline.PlaceRotationDegrees); } } internal static class ChestOwnership { internal enum Result { Ready, Pending, Unavailable } private sealed class State { public bool Registered; public int Request; public long Owner; public float Deadline; public bool Granted; public bool Denied; public ushort OwnerRevision; public uint DataRevision; } private const string RequestRpc = "VO_CraftChestRequest"; private const string ReplyRpc = "VO_CraftChestReply"; private static readonly ConditionalWeakTable<Container, State> States = new ConditionalWeakTable<Container, State>(); private static readonly Dictionary<string, Func<bool>> EnabledFeatures = new Dictionary<string, Func<bool>>(); private static int _nextRequest; public static void Install(Harmony harmony, string feature, Func<bool> enabled) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown EnabledFeatures[feature] = enabled; MethodInfo methodInfo = AccessTools.Method(typeof(Container), "Awake", Type.EmptyTypes, (Type[])null); MethodInfo register = AccessTools.Method(typeof(ChestOwnership), "Register", (Type[])null, (Type[])null); Patches patchInfo = Harmony.GetPatchInfo((MethodBase)methodInfo); if (patchInfo == null || !patchInfo.Postfixes.Any((Patch p) => p.owner == harmony.Id && p.PatchMethod == register)) { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(ChestOwnership), "Register", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static void Register(Container __instance) { ZNetView networkView = ChestScanner.GetNetworkView(__instance); if ((Object)(object)networkView == (Object)null || !networkView.IsValid()) { return; } State value = States.GetValue(__instance, (Container _) => new State()); if (!value.Registered) { value.Registered = true; networkView.Register<ZPackage>("VO_CraftChestRequest", (Action<long, ZPackage>)delegate(long sender, ZPackage package) { HandleRequest(__instance, sender, package); }); networkView.Register<ZPackage>("VO_CraftChestReply", (Action<long, ZPackage>)delegate(long sender, ZPackage package) { HandleReply(__instance, sender, package); }); } } public static Result Prepare(Container container, Player player) { //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Expected O, but got Unknown Register(container); ZNetView networkView = ChestScanner.GetNetworkView(container); if ((Object)(object)networkView == (Object)null || !networkView.IsValid() || ChestScanner.IsBusy(container, networkView) || !ChestScanner.HasContainerAccess(container, player.GetPlayerID())) { return Result.Unavailable; } State value = States.GetValue(container, (Container _) => new State()); ZDO zDO = networkView.GetZDO(); float realtimeSinceStartup = Time.realtimeSinceStartup; if (value.Request != 0) { if (value.Denied || realtimeSinceStartup >= value.Deadline) { value.Request = 0; return Result.Unavailable; } if (!value.Granted || !networkView.IsOwner() || zDO.OwnerRevision != value.OwnerRevision || zDO.DataRevision < value.DataRevision) { return Result.Pending; } value.Request = 0; ChestScanner.RefreshInventory(container); return Result.Ready; } if (networkView.IsOwner()) { ChestScanner.RefreshInventory(container); return Result.Ready; } long owner = zDO.GetOwner(); if (owner == 0L) { return Result.Pending; } value.Request = (_nextRequest = ((_nextRequest == int.MaxValue) ? 1 : (_nextRequest + 1))); value.Owner = owner; value.Deadline = realtimeSinceStartup + 5f; value.Granted = false; value.Denied = false; ZPackage val = new ZPackage(); val.Write(value.Request); val.Write(player.GetPlayerID()); networkView.InvokeRPC(owner, "VO_CraftChestRequest", new object[1] { val }); return Result.Pending; } private static void HandleRequest(Container container, long sender, ZPackage package) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_00d0: Unknown result type (might be due to invalid IL or missing references) int num; long playerId; try { num = package.ReadInt(); playerId = package.ReadLong(); } catch (Exception) { return; } if (num <= 0 || sender == 0L) { return; } ZNetView networkView = ChestScanner.GetNetworkView(container); if (!((Object)(object)networkView == (Object)null) && networkView.IsValid() && networkView.IsOwner()) { ZPackage val = new ZPackage(); val.Write(num); bool flag = EnabledFeatures.Values.Any((Func<bool> enabled) => enabled()) && !ChestScanner.IsBusy(container, networkView) && ChestScanner.HasContainerAccess(container, playerId); val.Write(flag); if (flag) { ZDO zDO = networkView.GetZDO(); zDO.SetOwner(sender); val.Write((int)zDO.OwnerRevision); val.Write((long)zDO.DataRevision); ZDOMan.instance.ForceSendZDO(sender, zDO.m_uid); } networkView.InvokeRPC(sender, "VO_CraftChestReply", new object[1] { val }); } } private static void HandleReply(Container container, long sender, ZPackage package) { if (!States.TryGetValue(container, out State value) || value.Request == 0 || sender != value.Owner) { return; } try { if (package.ReadInt() != value.Request) { return; } if (!package.ReadBool()) { value.Denied = true; return; } int num = package.ReadInt(); long num2 = package.ReadLong(); if (num >= 0 && num <= 65535 && num2 >= 0 && num2 <= uint.MaxValue) { value.OwnerRevision = (ushort)num; value.DataRevision = (uint)num2; value.Granted = true; } } catch (Exception) { } } } internal sealed class ChestScanner { private static readonly Func<Container, long, bool> CheckContainerAccess = AccessTools.MethodDelegate<Func<Container, long, bool>>(AccessTools.Method(typeof(Container), "CheckAccess", new Type[1] { typeof(long) }, (Type[])null) ?? throw new MissingMethodException("Container", "CheckAccess"), (object)null, true); private readonly List<Container> _cachedContainers = new List<Container>(); private readonly List<Inventory> _accessibleInventories = new List<Inventory>(); private readonly HashSet<Inventory> _seenInventories = new HashSet<Inventory>(); private readonly bool _includeRemote; private Player? _cachedPlayer; private Vector3 _cachedCenter; private float _cachedRange; private bool _cachedIgnoreWardedChests; private float _refreshAt; private static readonly Func<Container, bool> LoadInventory = AccessTools.MethodDelegate<Func<Container, bool>>(AccessTools.Method(typeof(Container), "Load", Type.EmptyTypes, (Type[])null), (object)null, true); public ChestScanner(bool includeRemote = false) { _includeRemote = includeRemote; } public Container? FindContainer(Inventory inventory) { foreach (Container cachedContainer in _cachedContainers) { if ((Object)(object)cachedContainer != (Object)null && cachedContainer.GetInventory() == inventory) { return cachedContainer; } } return null; } public bool TryPrepareInventory(Inventory inventory, Player player) { Container val = FindContainer(inventory); if ((Object)(object)val != (Object)null) { return ChestOwnership.Prepare(val, player) == ChestOwnership.Result.Ready; } return false; } public IReadOnlyList<Inventory> GetInventories(Player player, float range, bool ignoreWardedChests, float cacheSeconds) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) return GetInventories(player, ((Component)player).transform.position, range, ignoreWardedChests, cacheSeconds); } public IReadOnlyList<Inventory> GetInventories(Player player, Vector3 center, float range, bool ignoreWardedChests, float cacheSeconds) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) float num = Math.Max(1f, Math.Min(50f, range)); float num2 = Math.Max(1f, cacheSeconds); float realtimeSinceStartup = Time.realtimeSinceStartup; if (_cachedPlayer != player || _cachedCenter != center || _cachedRange != num || _cachedIgnoreWardedChests != ignoreWardedChests || realtimeSinceStartup >= _refreshAt) { Refresh(player, center, num, ignoreWardedChests, realtimeSinceStartup + num2); } RebuildAccessibleInventories(player, ignoreWardedChests); return _accessibleInventories; } private void Refresh(Player player, Vector3 center, float range, bool ignoreWardedChests, float refreshAt) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) _cachedContainers.Clear(); float num = range * range; Container[] array = Object.FindObjectsByType<Container>((FindObjectsSortMode)0); foreach (Container val in array) { if (!((Object)(object)val == (Object)null)) { Vector3 val2 = ((Component)val).transform.position - center; if (((Vector3)(ref val2)).sqrMagnitude <= num) { _cachedContainers.Add(val); } } } _cachedPlayer = player; _cachedCenter = center; _cachedRange = range; _cachedIgnoreWardedChests = ignoreWardedChests; _refreshAt = refreshAt; } private void RebuildAccessibleInventories(Player player, bool ignoreWardedChests) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) _accessibleInventories.Clear(); _seenInventories.Clear(); long playerID = player.GetPlayerID(); foreach (Container cachedContainer in _cachedContainers) { ZNetView val = (((Object)(object)cachedContainer == (Object)null) ? null : GetNetworkView(cachedContainer)); if (!((Object)(object)cachedContainer == (Object)null) && !((Object)(object)val == (Object)null) && val.IsValid() && (_includeRemote || cachedContainer.IsOwner()) && (!_includeRemote || !IsBusy(cachedContainer, val)) && (!cachedContainer.m_checkGuardStone || ignoreWardedChests || PrivateArea.CheckAccess(((Component)cachedContainer).transform.position, 0f, false, false)) && HasContainerAccess(cachedContainer, playerID)) { if (_includeRemote) { RefreshInventory(cachedContainer); } Inventory inventory = cachedContainer.GetInventory(); if (inventory != null && _seenInventories.Add(inventory)) { _accessibleInventories.Add(inventory); } } } } internal static void RefreshInventory(Container container) { LoadInventory(container); } internal static bool HasContainerAccess(Container container, long playerId) { return CheckContainerAccess(container, playerId); } internal static bool IsBusy(Container container, ZNetView networkView) { if (!container.IsInUse() && networkView.GetZDO().GetInt(ZDOVars.s_inUse, 0) == 0) { if ((Object)(object)container.m_wagon != (Object)null) { return container.m_wagon.InUse(); } return false; } return true; } internal static ZNetView? GetNetworkView(Container container) { if (!((Object)(object)container.m_rootObjectOverride != (Object)null)) { return ((Component)container).GetComponent<ZNetView>(); } return container.m_rootObjectOverride; } } public sealed class ContainerSizesModule : IFeatureModule { private sealed class ContainerSettings { public ConfigEntryInt Columns { get; } public ConfigEntryInt Rows { get; } public ContainerSettings(ConfigEntryInt columns, ConfigEntryInt rows) { Columns = columns; Rows = rows; } } private sealed class ContainerBaseline { public WeakReference<Container> Container { get; } public int Width { get; } public int Height { get; } public string? LastWarningSignature { get; set; } public ContainerBaseline(Container container) { Container = new WeakReference<Container>(container); Width = container.m_width; Height = container.m_height; } } private static readonly FieldInfo InventoryWidthField = AccessTools.Field(typeof(Inventory), "m_width") ?? throw new MissingFieldException("Inventory", "m_width"); private static readonly FieldInfo InventoryHeightField = AccessTools.Field(typeof(Inventory), "m_height") ?? throw new MissingFieldException("Inventory", "m_height"); private static ContainerSizesModule? _active; private readonly FeatureDefinition _feature; private readonly ModLogger _log; private readonly ContainerSettings _woodChest; private readonly ContainerSettings _personalChest; private readonly ContainerSettings _reinforcedChest; private readonly ContainerSettings _blackmetalChest; private readonly ContainerSettings _graustenChest; private readonly ContainerSettings _wardrobe; private readonly ContainerSettings _cart; private readonly ContainerSettings _karve; private readonly ContainerSettings _longship; private readonly List<ContainerBaseline> _baselines = new List<ContainerBaseline>(); private readonly HashSet<string> _loggedUnmatchedPrefabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); public string Name => "Container sizes"; public string Section => "ContainerSizes"; public bool IsEnabled => _feature.Enabled.Value; public FeatureClassification Classification => FeatureClassification.Synced; public ContainerSizesModule(FeatureRegistry registry, ModLogger log) { _feature = registry.Register(Name, Section, Classification); _log = log; _woodChest = AddSettings("WoodChest"); _personalChest = AddSettings("PersonalChest"); _reinforcedChest = AddSettings("ReinforcedChest"); _blackmetalChest = AddSettings("BlackmetalChest"); _graustenChest = AddSettings("GraustenChest"); _wardrobe = AddSettings("Wardrobe"); _cart = AddSettings("Cart"); _karve = AddSettings("Karve"); _longship = AddSettings("Longship"); registry.EffectiveValuesChanged += OnEffectiveValuesChanged; } public void ApplyPatches(Harmony harmony) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown //IL_0069: Expected O, but got Unknown _active = this; MethodInfo methodInfo = AccessTools.Method(typeof(Container), "Awake", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("Container", "Awake"); harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(ContainerSizesModule), "ContainerAwakePrefix", (Type[])null), new HarmonyMethod(typeof(ContainerSizesModule), "ContainerAwakePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private static void ContainerAwakePrefix(Container __instance) { ContainerSizesModule active = _active; if (active != null && active.IsEnabled) { active.ApplyBeforeAwake(__instance); } } private static void ContainerAwakePostfix(Container __instance) { ContainerSizesModule active = _active; if (active != null && active.IsEnabled) { ContainerBaseline containerBaseline = active.FindBaseline(__instance); if (containerBaseline != null && active.TryGetSettings(__instance, out ContainerSettings settings) && settings != null) { active.ApplyAfterLoad(__instance, containerBaseline, settings); } } } private ContainerSettings AddSettings(string keyPrefix) { return new ContainerSettings(_feature.Int(keyPrefix + "Columns", 0, "Absolute container columns. Zero leaves the vanilla value; nonzero values use their absolute value and clamp to 1-8."), _feature.Int(keyPrefix + "Rows", 0, "Absolute container rows. Zero leaves the vanilla value; nonzero values use their absolute value and clamp to 1-24.")); } private void ApplyBeforeAwake(Container container) { if (TryGetSettings(container, out ContainerSettings settings) && settings != null) { ContainerBaseline orAddBaseline = GetOrAddBaseline(container); int val = ResolveDimension(settings.Columns.Value, orAddBaseline.Width, 8); int val2 = ResolveDimension(settings.Rows.Value, orAddBaseline.Height, 24); container.m_width = Math.Max(val, orAddBaseline.Width); container.m_height = Math.Max(val2, orAddBaseline.Height); } } private void ApplyAfterLoad(Container container, ContainerBaseline baseline, ContainerSettings settings) { int requestedWidth = ResolveDimension(settings.Columns.Value, baseline.Width, 8); int requestedHeight = ResolveDimension(settings.Rows.Value, baseline.Height, 24); ApplyLoadedDimensions(container, baseline, requestedWidth, requestedHeight); } private void ApplyLoadedDimensions(Container container, ContainerBaseline baseline, int requestedWidth, int requestedHeight) { Inventory inventory = container.GetInventory(); if (inventory == null) { return; } int num = 0; int num2 = 0; int num3 = 0; foreach (ItemData allItem in inventory.GetAllItems()) { num++; num2 = Math.Max(num2, allItem.m_gridPos.x + 1); num3 = Math.Max(num3, allItem.m_gridPos.y + 1); } int val = requestedWidth; int val2 = requestedHeight; if (num != 0) { val = Math.Max(val, baseline.Width); val2 = Math.Max(val2, baseline.Height); } val = Math.Max(val, num2); val2 = Math.Max(val2, num3); container.m_width = val; container.m_height = val2; InventoryWidthField.SetValue(inventory, val); InventoryHeightField.SetValue(inventory, val2); if (val != requestedWidth || val2 != requestedHeight) { string prefabName = Utils.GetPrefabName(((Object)((Component)container).gameObject).name); string text = requestedWidth + "x" + requestedHeight + ":" + val + "x" + val2; if (!string.Equals(baseline.LastWarningSignature, text, StringComparison.Ordinal)) { _log.Warning("ContainerSizes kept '" + prefabName + "' (" + container.m_name + ") at " + $"{val}x{val2} instead of requested " + $"{requestedWidth}x{requestedHeight} because it contains {num} item(s); " + "the grid was not shrunk so no occupied slot can be lost."); baseline.LastWarningSignature = text; } } else { baseline.LastWarningSignature = null; } } private void OnEffectiveValuesChanged() { Container[] array = Object.FindObjectsOfType<Container>(); foreach (Container container in array) { ContainerBaseline containerBaseline = FindBaseline(container); if (IsEnabled) { if (TryGetSettings(container, out ContainerSettings settings) && settings != null) { if (containerBaseline == null) { containerBaseline = AddBaseline(container); } ApplyAfterLoad(container, containerBaseline, settings); } } else if (containerBaseline != null) { ApplyLoadedDimensions(container, containerBaseline, containerBaseline.Width, containerBaseline.Height); } } } private bool TryGetSettings(Container container, out ContainerSettings? settings) { string prefabName = Utils.GetPrefabName(((Object)((Component)container).gameObject).name); switch (prefabName.ToLowerInvariant()) { case "piece_chest_wood": settings = _woodChest; return true; case "piece_chest_private": settings = _personalChest; return true; case "piece_chest": settings = _reinforcedChest; return true; case "piece_chest_blackmetal": settings = _blackmetalChest; return true; case "piece_chest_grausten": settings = _graustenChest; return true; case "piece_chest_warderobe": settings = _wardrobe; return true; case "cart": settings = _cart; return true; case "karve": settings = _karve; return true; case "vikingship": settings = _longship; return true; default: settings = null; LogUnmatchedPrefab(prefabName); return false; } } private void LogUnmatchedPrefab(string prefabName) { string text = (string.IsNullOrEmpty(prefabName) ? "<empty>" : prefabName); if (_loggedUnmatchedPrefabs.Add(text)) { _log.Debug("ContainerSizes saw unmatched Container prefab '" + text + "'; its grid dimensions were left unchanged."); } } private ContainerBaseline GetOrAddBaseline(Container container) { return FindBaseline(container) ?? AddBaseline(container); } private ContainerBaseline? FindBaseline(Container container) { for (int num = _baselines.Count - 1; num >= 0; num--) { ContainerBaseline containerBaseline = _baselines[num]; if (!containerBaseline.Container.TryGetTarget(out Container target) || (Object)(object)target == (Object)null) { _baselines.RemoveAt(num); } else if (target == container) { return containerBaseline; } } return null; } private ContainerBaseline AddBaseline(Container container) { ContainerBaseline containerBaseline = new ContainerBaseline(container); _baselines.Add(containerBaseline); return containerBaseline; } private static int ResolveDimension(int configured, int vanilla, int maximum) { if (configured == 0) { return vanilla; } long num = ((configured < 0) ? (-configured) : configured); if (num < 1) { return 1; } if (num <= maximum) { return (int)num; } return maximum; } } public sealed class CookingStationModule : IFeatureModule { private readonly struct UpdateCookingState { public bool Changed { get; } public bool RequireFire { get; } public UpdateCookingState(bool requireFire) { Changed = true; RequireFire = requireFire; } } private sealed class CookingStationBaseline { public WeakReference<CookingStation> Station { get; } public bool RequireFire { get; } public List<ConversionBaseline> Conversions { get; } public CookingStationBaseline(CookingStation station) { Station = new WeakReference<CookingStation>(station); RequireFire = station.m_requireFire; Conversions = new List<ConversionBaseline>(); foreach (ItemConversion item in station.m_conversion) { if (item != null) { Conversions.Add(new ConversionBaseline(item)); } } } public void Restore(CookingStation station) { station.m_requireFire = RequireFire; foreach (ConversionBaseline conversion in Conversions) { conversion.Conversion.m_cookTime = conversion.CookTime; } } } private sealed class ConversionBaseline { public ItemConversion Conversion { get; } public float CookTime { get; } public ConversionBaseline(ItemConversion conversion) { Conversion = conversion; CookTime = conversion.m_cookTime; } } private sealed class StationState { public ChestScanner Scanner { get; } = new ChestScanner(includeRemote: true); public float NextCheckAt { get; set; } } private const string AddFuelRpc = "RPC_AddFuel"; private const string AddItemRpc = "RPC_AddItem"; private static readonly Func<CookingStation, float> GetFuel = AccessTools.MethodDelegate<Func<CookingStation, float>>(AccessTools.Method(typeof(CookingStation), "GetFuel", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("CookingStation", "GetFuel"), (object)null, true); private static readonly Func<CookingStation, int> GetFreeSlot = AccessTools.MethodDelegate<Func<CookingStation, int>>(AccessTools.Method(typeof(CookingStation), "GetFreeSlot", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("CookingStation", "GetFreeSlot"), (object)null, true); private static readonly Func<CookingStation, bool> IsFireLit = AccessTools.MethodDelegate<Func<CookingStation, bool>>(AccessTools.Method(typeof(CookingStation), "IsFireLit", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("CookingStation", "IsFireLit"), (object)null, true); private static readonly Func<CookingStation, Inventory, ItemData?> FindCookableItem = AccessTools.MethodDelegate<Func<CookingStation, Inventory, ItemData>>(AccessTools.Method(typeof(CookingStation), "FindCookableItem", new Type[1] { typeof(Inventory) }, (Type[])null) ?? throw new MissingMethodException("CookingStation", "FindCookableItem"), (object)null, true); private static CookingStationModule? _active; private readonly FeatureDefinition _feature; private readonly ConfigEntryPercent _cookSpeedMultiplier; private readonly ConfigEntryBool _ignoreFireRequirement; private readonly ConfigEntryBool _autoFuel; private readonly ConfigEntryBool _autoFeedRaw; private readonly ConfigEntryFloat _range; private readonly ConfigEntryFloat _checkIntervalSeconds; private readonly List<CookingStationBaseline> _baselines = new List<CookingStationBaseline>(); private readonly ConditionalWeakTable<CookingStation, StationState> _stationStates = new ConditionalWeakTable<CookingStation, StationState>(); public string Name => "Cooking station"; public string Section => "CookingStation"; public bool IsEnabled => _feature.Enabled.Value; public FeatureClassification Classification => FeatureClassification.Synced; public CookingStationModule(FeatureRegistry registry) { _feature = registry.Register(Name, Section, Classification); _cookSpeedMultiplier = _feature.Percent("CookSpeedMultiplier", 0f, "Scale each cooking conversion's per-item cook time."); _ignoreFireRequirement = _feature.Bool("IgnoreFireRequirement", defaultValue: false, "Allow cooking stations to operate without their normal fire requirement."); _autoFuel = _feature.Bool("AutoFuel", defaultValue: false, "Pull fuel from nearby containers for fuel-using cooking stations such as stone ovens."); _autoFeedRaw = _feature.Bool("AutoFeedRaw", defaultValue: false, "Pull accepted raw items from nearby containers into free cooking slots."); _range = _feature.Float("Range", 10f, "Maximum distance in metres from the station to a source container. Clamped to 1-50."); _checkIntervalSeconds = _feature.Float("CheckIntervalSeconds", 2f, "Seconds between automation checks for each station. Values below 1 are clamped to 1."); registry.EffectiveValuesChanged += OnEffectiveValuesChanged; } public void ApplyPatches(Harmony harmony) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: 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_00b5: Expected O, but got Unknown //IL_00b5: Expected O, but got Unknown //IL_00b5: Expected O, but got Unknown _active = this; ChestOwnership.Install(harmony, Section, () => IsEnabled); PatchPostfix(harmony, "Awake", "CookingStationAwakePostfix"); PatchPostfix(harmony, "IsFireLit", "IsFireLitPostfix"); MethodInfo methodInfo = AccessTools.Method(typeof(CookingStation), "UpdateCooking", Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("CookingStation", "UpdateCooking"); harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(CookingStationModule), "UpdateCookingPrefix", (Type[])null), new HarmonyMethod(typeof(CookingStationModule), "UpdateCookingPostfix", (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(CookingStationModule), "UpdateCookingFinalizer", (Type[])null), (HarmonyMethod)null); } private static void PatchPostfix(Harmony harmony, string methodName, string postfixName) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(CookingStation), methodName, Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException("CookingStation", methodNa