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 Inferno v0.3.2
plugins/Inferno.Core.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using Inferno.Core.Areas; using Inferno.Core.Catalog; using Inferno.Core.Commands; using Inferno.Core.Fuel; using Inferno.Core.Permissions; using Inferno.Core.Settings; using Inferno.Core.Time; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("GrundleLord")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 GrundleLord. MIT License.")] [assembly: AssemblyFileVersion("0.3.2.0")] [assembly: AssemblyInformationalVersion("0.3.2+4c3e115387ee3266bdea5a26431267084f7f433c")] [assembly: AssemblyProduct("Inferno")] [assembly: AssemblyTitle("Inferno.Core")] [assembly: InternalsVisibleTo("Inferno.Core.Tests")] [assembly: AssemblyVersion("0.3.2.0")] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlyArray<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => _items.Length; T IReadOnlyList<T>.this[int index] => _items[index]; int ICollection<T>.Count => _items.Length; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return ((IEnumerable<T>)_items).GetEnumerator(); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return ((ICollection<T>)_items).Contains(item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { ((ICollection<T>)_items).CopyTo(array, arrayIndex); } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { return ((IList<T>)_items).IndexOf(item); } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T> { object IEnumerator.Current => _item; T IEnumerator<T>.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => 1; T IReadOnlyList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection<T>.Count => 1; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer<T>.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer<T>.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return new Enumerator(_item); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return EqualityComparer<T>.Default.Equals(_item, item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { if (!EqualityComparer<T>.Default.Equals(_item, item)) { return -1; } return 0; } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Inferno.Core.Time { public static class ClockSetting { public const int MaxHour = 24; public const int MaxMinute = 60; public static TimeOfDay ToTimeOfDay(int hour, int minute) { if ((hour < 0 || hour > 24) ? true : false) { throw new ArgumentOutOfRangeException("hour", hour, "Hour must be 0 to 24."); } if ((minute < 0 || minute > 60) ? true : false) { throw new ArgumentOutOfRangeException("minute", minute, "Minute must be 0 to 60."); } int num = (hour * 60 + minute) % 1440; return TimeOfDay.FromHourMinute(num / 60, num % 60); } public static bool TryParse(string? text, out TimeOfDay result) { result = default(TimeOfDay); string[] array = text?.Trim().Split(new char[1] { ':' }); bool flag = array == null || array.Length != 2; if (!flag) { int length = array[0].Length; bool flag2 = ((length < 1 || length > 2) ? true : false); flag = flag2; } if (flag || array[1].Length != 2) { return false; } if (!TimeOfDay.TryParseDigits(array[0], out var value) || !TimeOfDay.TryParseDigits(array[1], out var value2) || value > 24 || value2 > 60) { return false; } result = ToTimeOfDay(value, value2); return true; } } public readonly struct DailySchedule : IEquatable<DailySchedule> { public static DailySchedule AlwaysOn => default(DailySchedule); public static DailySchedule Night => new DailySchedule(TimeOfDay.FromHourMinute(18, 0), TimeOfDay.FromHourMinute(6, 0)); public static DailySchedule Day => new DailySchedule(TimeOfDay.FromHourMinute(6, 0), TimeOfDay.FromHourMinute(18, 0)); public TimeOfDay OnTime { get; } public TimeOfDay OffTime { get; } public bool IsAlwaysOn => OnTime == OffTime; public DailySchedule(TimeOfDay onTime, TimeOfDay offTime) { OnTime = onTime; OffTime = offTime; } public bool IsOnAt(TimeOfDay now) { int minutesSinceMidnight = OnTime.MinutesSinceMidnight; int minutesSinceMidnight2 = OffTime.MinutesSinceMidnight; int minutesSinceMidnight3 = now.MinutesSinceMidnight; if (minutesSinceMidnight == minutesSinceMidnight2) { return true; } if (minutesSinceMidnight >= minutesSinceMidnight2) { if (minutesSinceMidnight3 < minutesSinceMidnight) { return minutesSinceMidnight3 < minutesSinceMidnight2; } return true; } if (minutesSinceMidnight3 >= minutesSinceMidnight) { return minutesSinceMidnight3 < minutesSinceMidnight2; } return false; } public override string ToString() { if (!IsAlwaysOn) { return $"{OnTime}-{OffTime}"; } return "always on"; } public bool Equals(DailySchedule other) { if (OnTime == other.OnTime) { return OffTime == other.OffTime; } return false; } public override bool Equals(object? obj) { if (obj is DailySchedule other) { return Equals(other); } return false; } public override int GetHashCode() { return OnTime.MinutesSinceMidnight * 1440 + OffTime.MinutesSinceMidnight; } public static bool operator ==(DailySchedule left, DailySchedule right) { return left.Equals(right); } public static bool operator !=(DailySchedule left, DailySchedule right) { return !left.Equals(right); } } public static class GameClock { private const double RawSunrise = 0.15; private const double RawSunset = 0.85; public static TimeOfDay TimeAt(double worldTimeSeconds, long dayLengthSeconds) { return TimeOfDay.FromDayFraction(DisplayedDayFraction(worldTimeSeconds, dayLengthSeconds)); } public static double DisplayedDayFraction(double worldTimeSeconds, long dayLengthSeconds) { if (!(worldTimeSeconds >= 0.0) || double.IsInfinity(worldTimeSeconds)) { throw new ArgumentOutOfRangeException("worldTimeSeconds", worldTimeSeconds, "World time must be a finite value ≥ 0."); } if (dayLengthSeconds <= 0) { throw new ArgumentOutOfRangeException("dayLengthSeconds", dayLengthSeconds, "Day length must be > 0."); } return Rescale(worldTimeSeconds % (double)dayLengthSeconds / (double)dayLengthSeconds); } internal static double Rescale(double raw) { if (raw >= 0.15 && raw <= 0.85) { return 0.25 + (raw - 0.15) / 0.7 * 0.5; } if (raw < 0.5) { return raw / 0.15 * 0.25; } return 0.75 + (raw - 0.85) / 0.15000000000000002 * 0.25; } } public readonly struct TimeOfDay : IEquatable<TimeOfDay> { public const int MinutesPerDay = 1440; public int MinutesSinceMidnight { get; } public int Hour => MinutesSinceMidnight / 60; public int Minute => MinutesSinceMidnight % 60; private TimeOfDay(int minutesSinceMidnight) { MinutesSinceMidnight = minutesSinceMidnight; } public static TimeOfDay FromHourMinute(int hour, int minute) { if ((hour < 0 || hour > 23) ? true : false) { throw new ArgumentOutOfRangeException("hour", hour, "Hour must be 0 to 23."); } if ((minute < 0 || minute > 59) ? true : false) { throw new ArgumentOutOfRangeException("minute", minute, "Minute must be 0 to 59."); } return new TimeOfDay(hour * 60 + minute); } public static TimeOfDay FromDayFraction(double fraction) { if (!(fraction >= 0.0) || !(fraction <= 1.0)) { throw new ArgumentOutOfRangeException("fraction", fraction, "Day fraction must be 0 to 1."); } return new TimeOfDay((int)Math.Floor(fraction * 1440.0) % 1440); } public static bool TryParse(string? text, out TimeOfDay result) { result = default(TimeOfDay); if (text == null) { return false; } string[] array = text.Trim().Split(new char[1] { ':' }); bool flag = array.Length != 2; if (!flag) { int length = array[0].Length; bool flag2 = ((length < 1 || length > 2) ? true : false); flag = flag2; } if (flag || array[1].Length != 2) { return false; } if (!TryParseDigits(array[0], out var value) || !TryParseDigits(array[1], out var value2)) { return false; } if (value > 23 || value2 > 59) { return false; } result = new TimeOfDay(value * 60 + value2); return true; } public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0:00}:{1:00}", Hour, Minute); } public bool Equals(TimeOfDay other) { return MinutesSinceMidnight == other.MinutesSinceMidnight; } public override bool Equals(object? obj) { if (obj is TimeOfDay other) { return Equals(other); } return false; } public override int GetHashCode() { return MinutesSinceMidnight; } public static bool operator ==(TimeOfDay left, TimeOfDay right) { return left.Equals(right); } public static bool operator !=(TimeOfDay left, TimeOfDay right) { return !left.Equals(right); } internal static bool TryParseDigits(string text, out int value) { value = 0; foreach (char c in text) { if ((c < '0' || c > '9') ? true : false) { return false; } value = value * 10 + (c - 48); } return true; } } } namespace Inferno.Core.Sync { public static class MenuCommands { public static IReadOnlyList<string> ForItem(string prefabName, ItemSettings before, ItemSettings after) { if (string.IsNullOrWhiteSpace(prefabName) || prefabName.IndexOfAny(new char[4] { ' ', '\t', '\n', '\r' }) >= 0) { throw new ArgumentException("Prefab name must be a single word.", "prefabName"); } if (before == null) { throw new ArgumentNullException("before"); } if (after == null) { throw new ArgumentNullException("after"); } List<string> list = new List<string>(); if (before.BurnRateLevel != after.BurnRateLevel) { list.Add(Command("burnrate", prefabName, after.BurnRateLevel.ToString(CultureInfo.InvariantCulture))); } if (before.Schedule != after.Schedule) { list.Add(after.Schedule.IsAlwaysOn ? Command("schedule", prefabName, "off") : Command("schedule", prefabName, after.Schedule.OnTime.ToString(), after.Schedule.OffTime.ToString())); } if (before.Smoke != after.Smoke) { list.Add(Command("smoke", prefabName, OnOff(after.Smoke))); } if (before.AlwaysOn != after.AlwaysOn) { list.Add(Command("alwayson", prefabName, OnOff(after.AlwaysOn))); } return list; } public static IReadOnlyList<string> ForGeneral(GeneralSettings before, GeneralSettings after) { if (before == null) { throw new ArgumentNullException("before"); } if (after == null) { throw new ArgumentNullException("after"); } List<string> list = new List<string>(); AddIfChanged(list, "adminonly", before.AdminOnly, after.AdminOnly); AddIfChanged(list, "hidecommands", before.HideCommands, after.HideCommands); AddIfChanged(list, "ignorerain", before.IgnoreRain, after.IgnoreRain); AddIfChanged(list, "serverownership", before.ServerOwnership, after.ServerOwnership); return list; } private static void AddIfChanged(List<string> commands, string name, bool before, bool after) { if (before != after) { commands.Add(Command(name, OnOff(after))); } } private static string Command(params string[] parts) { return "!fires " + string.Join(" ", parts); } private static string OnOff(bool value) { if (!value) { return "off"; } return "on"; } } public sealed class SettingsMessage { public const int ProtocolVersion = 1; private const string Header = "inferno-settings"; private const char Field = '\t'; private const char Line = '\n'; public bool CanEdit { get; } public GeneralSettings General { get; } public IReadOnlyDictionary<string, ItemSettings> Items { get; } public SettingsMessage(bool canEdit, GeneralSettings general, IReadOnlyDictionary<string, ItemSettings> items) { CanEdit = canEdit; General = general ?? throw new ArgumentNullException("general"); Items = items ?? throw new ArgumentNullException("items"); } public string Encode() { StringBuilder stringBuilder = new StringBuilder(); Append(stringBuilder, "inferno-settings", Number(1)); Append(stringBuilder, "canedit", Bit(CanEdit)); Append(stringBuilder, "general", Bit(General.AdminOnly), Bit(General.HideCommands), Bit(General.IgnoreRain), Bit(General.ServerOwnership)); foreach (KeyValuePair<string, ItemSettings> item in Items) { if (item.Key.IndexOf('\t') >= 0 || item.Key.IndexOf('\n') >= 0) { throw new ArgumentException("Prefab name '" + item.Key + "' contains a tab or newline.", "Items"); } ItemSettings value = item.Value; Append(stringBuilder, "item", item.Key, Bit(value.AlwaysOn), Number(value.BurnRateLevel), Number(value.Schedule.OnTime.MinutesSinceMidnight), Number(value.Schedule.OffTime.MinutesSinceMidnight), Bit(value.Smoke)); } return stringBuilder.ToString(); } public static bool TryDecode(string? text, out SettingsMessage message) { message = null; if (text == null) { return false; } string[] array = text.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 3 || !TryFields(array[0], "inferno-settings", 1, out string[] fields) || !TryInt(fields[0], out var value) || value != 1 || !TryFields(array[1], "canedit", 1, out string[] fields2) || !TryBit(fields2[0], out var value2) || !TryFields(array[2], "general", 4, out string[] fields3) || !TryBit(fields3[0], out var value3) || !TryBit(fields3[1], out var value4) || !TryBit(fields3[2], out var value5) || !TryBit(fields3[3], out var value6)) { return false; } Dictionary<string, ItemSettings> dictionary = new Dictionary<string, ItemSettings>(StringComparer.OrdinalIgnoreCase); for (int i = 3; i < array.Length; i++) { if (!TryItem(array[i], out string name, out ItemSettings settings) || dictionary.ContainsKey(name)) { return false; } dictionary.Add(name, settings); } message = new SettingsMessage(value2, new GeneralSettings(value3, value4, value5, value6), dictionary); return true; } private static bool TryItem(string line, out string name, out ItemSettings settings) { name = string.Empty; settings = null; string[] fields; bool value = default(bool); int value2 = default(int); bool flag = !TryFields(line, "item", 6, out fields) || fields[0].Length == 0 || !TryBit(fields[1], out value) || !TryInt(fields[2], out value2); if (!flag) { bool flag2 = ((value2 < -10 || value2 > 10) ? true : false); flag = flag2; } if (flag || !TryMinutes(fields[3], out var time) || !TryMinutes(fields[4], out var time2) || !TryBit(fields[5], out var value3)) { return false; } name = fields[0]; settings = new ItemSettings(value, value2, new DailySchedule(time, time2), value3); return true; } private static bool TryFields(string line, string tag, int count, out string[] fields) { string[] array = line.Split(new char[1] { '\t' }); fields = new string[count]; if (array.Length != count + 1 || array[0] != tag) { return false; } Array.Copy(array, 1, fields, 0, count); return true; } private static bool TryMinutes(string text, out TimeOfDay time) { time = default(TimeOfDay); int value; bool flag = !TryInt(text, out value); if (!flag) { bool flag2 = ((value < 0 || value >= 1440) ? true : false); flag = flag2; } if (flag) { return false; } time = TimeOfDay.FromHourMinute(value / 60, value % 60); return true; } private static bool TryBit(string text, out bool value) { value = text == "1"; if (!value) { return text == "0"; } return true; } private static bool TryInt(string text, out int value) { return int.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out value); } private static string Bit(bool value) { if (!value) { return "0"; } return "1"; } private static string Number(int value) { return value.ToString(CultureInfo.InvariantCulture); } private static void Append(StringBuilder sb, params string[] fields) { sb.Append(string.Join('\t'.ToString(), fields)); sb.Append('\n'); } } } namespace Inferno.Core.Settings { public static class BurnRate { public const int Min = -10; public const int Max = 10; public const int Vanilla = 0; public static double ToMultiplier(int level) { if ((level < -10 || level > 10) ? true : false) { throw new ArgumentOutOfRangeException("level", level, "Burn rate must be -10 to 10."); } return 1.0 + (double)level / 10.0; } } public sealed class GeneralSettings { public static GeneralSettings Default { get; } = new GeneralSettings(adminOnly: false, hideCommands: true, ignoreRain: false); public bool AdminOnly { get; } public bool HideCommands { get; } public bool IgnoreRain { get; } public bool ServerOwnership { get; } public GeneralSettings(bool adminOnly, bool hideCommands, bool ignoreRain, bool serverOwnership = false) { AdminOnly = adminOnly; HideCommands = hideCommands; IgnoreRain = ignoreRain; ServerOwnership = serverOwnership; } public GeneralSettings WithAdminOnly(bool value) { return new GeneralSettings(value, HideCommands, IgnoreRain, ServerOwnership); } public GeneralSettings WithHideCommands(bool value) { return new GeneralSettings(AdminOnly, value, IgnoreRain, ServerOwnership); } public GeneralSettings WithIgnoreRain(bool value) { return new GeneralSettings(AdminOnly, HideCommands, value, ServerOwnership); } public GeneralSettings WithServerOwnership(bool value) { return new GeneralSettings(AdminOnly, HideCommands, IgnoreRain, value); } } public interface ISettingsStore { GeneralSettings General { get; set; } ItemSettings GetItem(string prefabName); void SetItem(string prefabName, ItemSettings settings); ItemSettings? GetObject(string objectKey); void SetObject(string objectKey, ItemSettings? settings); } public enum ItemKind { LightSource, FuelStation } public sealed class ItemSettings : IEquatable<ItemSettings> { public static ItemSettings Vanilla { get; } = new ItemSettings(alwaysOn: false, 0, DailySchedule.AlwaysOn); public static ItemSettings AlwaysOnDefault { get; } = new ItemSettings(alwaysOn: true, 0, DailySchedule.AlwaysOn); public bool AlwaysOn { get; } public int BurnRateLevel { get; } public double BurnMultiplier => BurnRate.ToMultiplier(BurnRateLevel); public DailySchedule Schedule { get; } public bool Smoke { get; } public ItemSettings(bool alwaysOn, int burnRate, DailySchedule schedule, bool smoke = true) { BurnRate.ToMultiplier(burnRate); AlwaysOn = alwaysOn; BurnRateLevel = burnRate; Schedule = schedule; Smoke = smoke; } public static ItemSettings DefaultFor(ItemKind kind) { if (kind != ItemKind.LightSource) { return Vanilla; } return AlwaysOnDefault; } public ItemSettings WithAlwaysOn(bool value) { return new ItemSettings(value, BurnRateLevel, Schedule, Smoke); } public ItemSettings WithBurnRate(int value) { return new ItemSettings(AlwaysOn, value, Schedule, Smoke); } public ItemSettings WithSchedule(DailySchedule value) { return new ItemSettings(AlwaysOn, BurnRateLevel, value, Smoke); } public ItemSettings WithSmoke(bool value) { return new ItemSettings(AlwaysOn, BurnRateLevel, Schedule, value); } public override string ToString() { return $"alwayson={OnOff(AlwaysOn)} burnrate={BurnRateLevel} schedule={Schedule} smoke={OnOff(Smoke)}"; } public bool Equals(ItemSettings? other) { if (other != null && AlwaysOn == other.AlwaysOn && BurnRateLevel == other.BurnRateLevel && Schedule == other.Schedule) { return Smoke == other.Smoke; } return false; } public override bool Equals(object? obj) { return Equals(obj as ItemSettings); } public override int GetHashCode() { return Schedule.GetHashCode() * 128 + (BurnRateLevel - -10) * 4 + (AlwaysOn ? 2 : 0) + (Smoke ? 1 : 0); } private static string OnOff(bool value) { if (!value) { return "off"; } return "on"; } } public sealed class Preset { private readonly Func<CatalogItem, ItemSettings, ItemSettings?> _apply; public string Name { get; } public string DefaultTarget { get; } public string Description { get; } internal Preset(string name, string defaultTarget, string description, Func<CatalogItem, ItemSettings, ItemSettings?> apply) { Name = name; DefaultTarget = defaultTarget; Description = description; _apply = apply; } public ItemSettings? Apply(CatalogItem item, ItemSettings current) { return _apply(item, current); } } public static class Presets { public static Preset Eternal { get; } = new Preset("eternal", "lights", "always on, never needs fuel (default for lights)", (CatalogItem _, ItemSettings s) => s.WithAlwaysOn(value: true).WithBurnRate(0).WithSchedule(DailySchedule.AlwaysOn)); public static Preset Night { get; } = new Preset("night", "lights", "lit 18:00-06:00, never uses fuel", (CatalogItem item, ItemSettings s) => (!item.CanSchedule) ? null : s.WithAlwaysOn(value: false).WithBurnRate(-10).WithSchedule(DailySchedule.Night)); public static Preset Vanilla { get; } = new Preset("vanilla", "all", "plain game behaviour", (CatalogItem _, ItemSettings s) => s.WithAlwaysOn(value: false).WithBurnRate(0).WithSchedule(DailySchedule.AlwaysOn)); public static IReadOnlyList<Preset> All { get; } = new <>z__ReadOnlyArray<Preset>(new Preset[3] { Eternal, Night, Vanilla }); public static bool TryGet(string? name, out Preset preset) { foreach (Preset item in All) { if (string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)) { preset = item; return true; } } preset = null; return false; } } public static class SettingsDiff { public static IReadOnlyList<SettingChange> Compare(SettingsSnapshot before, SettingsSnapshot after) { if (before == null) { throw new ArgumentNullException("before"); } if (after == null) { throw new ArgumentNullException("after"); } List<SettingChange> list = new List<SettingChange>(); AddIfChanged(list, "AdminOnly", before.General.AdminOnly, after.General.AdminOnly); AddIfChanged(list, "HideCommands", before.General.HideCommands, after.General.HideCommands); AddIfChanged(list, "IgnoreRain", before.General.IgnoreRain, after.General.IgnoreRain); AddIfChanged(list, "ServerOwnership", before.General.ServerOwnership, after.General.ServerOwnership); foreach (KeyValuePair<string, ItemSettings> item in after.Items) { if (before.Items.TryGetValue(item.Key, out ItemSettings value) && !value.Equals(item.Value)) { list.Add(new SettingChange(item.Key, "Settings", value.ToString(), item.Value.ToString())); } } return list; } private static void AddIfChanged(List<SettingChange> changes, string name, bool before, bool after) { if (before != after) { changes.Add(new SettingChange("General", name, before ? "on" : "off", after ? "on" : "off")); } } } public sealed class SettingsSnapshot { public GeneralSettings General { get; } public IReadOnlyDictionary<string, ItemSettings> Items { get; } public SettingsSnapshot(GeneralSettings general, IReadOnlyDictionary<string, ItemSettings> items) { General = general ?? throw new ArgumentNullException("general"); Items = items ?? throw new ArgumentNullException("items"); } } } namespace Inferno.Core.Permissions { public static class PermissionPolicy { public static bool CanChangeSettings(bool isAdmin, bool adminOnly) { if (!isAdmin) { return !adminOnly; } return true; } } } namespace Inferno.Core.Ownership { public static class FireplaceRpc { public const int StateOn = 1; public const int StateOff = 2; public static float AddOne(float fuel, float maxFuel) { fuel = Sanitize(fuel); if (Math.Ceiling(fuel) >= (double)maxFuel) { return fuel; } return Clamp(Clamp(fuel, maxFuel) + 1f, maxFuel); } public static float AddAmount(float fuel, float amount, float maxFuel) { return Clamp(Sanitize(fuel) + Sanitize(amount, allowNegative: true), maxFuel); } public static float SetAmount(float fuel, float maxFuel) { return Clamp(Sanitize(fuel), maxFuel); } public static int Toggle(int state) { if (state != 1) { return 1; } return 2; } private static float Clamp(float value, float max) { return Math.Max(0f, Math.Min(max, value)); } private static float Sanitize(float value, bool allowNegative = false) { if (!float.IsNaN(value) && !float.IsInfinity(value) && (allowNegative || !(value < 0f))) { return value; } return 0f; } } public sealed class LoanBook<TKey> where TKey : notnull { private readonly Dictionary<TKey, double> _until = new Dictionary<TKey, double>(); public int Count => _until.Count; public void Lend(TKey id, double now, double seconds) { if (!(seconds > 0.0)) { throw new ArgumentOutOfRangeException("seconds", seconds, "Loan time must be > 0."); } double num = now + seconds; _until[id] = (_until.TryGetValue(id, out var value) ? Math.Max(value, num) : num); } public bool IsOnLoan(TKey id, double now) { if (_until.TryGetValue(id, out var value)) { return now < value; } return false; } public void Prune(double now) { List<TKey> list = null; foreach (KeyValuePair<TKey, double> item in _until) { if (now >= item.Value) { (list ?? (list = new List<TKey>())).Add(item.Key); } } if (list == null) { return; } foreach (TKey item2 in list) { _until.Remove(item2); } } public void Clear() { _until.Clear(); } } public enum OwnershipAction { None, Claim, Release } public static class OwnershipPolicy { public static OwnershipAction Decide(bool modeEnabled, bool eligible, bool ownedByServer, bool onLoan) { if (!modeEnabled || !eligible) { if (!ownedByServer) { return OwnershipAction.None; } return OwnershipAction.Release; } if (!(onLoan || ownedByServer)) { return OwnershipAction.Claim; } return OwnershipAction.None; } } } namespace Inferno.Core.Fuel { public static class FuelController { public const double UnattendedStepSeconds = 30.0; private const float Epsilon = 0.0001f; private const float RefillMargin = 0.5f; public static FuelDecision Decide(ItemSettings settings, FuelItemInfo item, FuelState state, float? baseline, TimeOfDay now, bool ignoreRain) { if (settings == null) { throw new ArgumentNullException("settings"); } if (item == null) { throw new ArgumentNullException("item"); } float num = ((state.Fuel >= 0f) ? state.Fuel : 0f); DecideSwitch(settings, item, state, now, ignoreRain, out var setSwitchedOn, out var setMarker); bool flag = state.IsUnattended && item.BurnsOverTime && state.SecondsSinceBurnClock >= 30.0; bool flag2 = false; float? num2; if (settings.AlwaysOn) { flag2 = num <= item.MaxFuel - Math.Min(0.5f, item.MaxFuel / 2f); num2 = (flag2 ? new float?(item.MaxFuel) : ((float?)null)); } else { num2 = ScaleFuelLoss(settings.BurnMultiplier, item.MaxFuel, num, baseline); if (flag && state.IsSwitchedOn) { num2 = BurnUnattended(num2 ?? num, state.SecondsSinceBurnClock, item.SecondsPerFuel, settings.BurnMultiplier) ?? num2; } } bool mustWin = !state.IsUnattended && (flag2 || setSwitchedOn.HasValue); return new FuelDecision(num2, setSwitchedOn, setMarker, flag, num2 ?? num, mustWin); } public static FuelDecision RestoreForShutdown(FuelState state) { if (!state.SwitchedOffByInferno) { return new FuelDecision(null, null, null, refreshBurnClock: false, state.Fuel); } return new FuelDecision(null, state.IsSwitchedOn ? ((bool?)null) : new bool?(true), false, refreshBurnClock: false, state.Fuel); } private static void DecideSwitch(ItemSettings settings, FuelItemInfo item, FuelState state, TimeOfDay now, bool ignoreRain, out bool? setSwitchedOn, out bool? setMarker) { setSwitchedOn = null; setMarker = null; if (!item.HasOnOffSwitch) { return; } if (!settings.AlwaysOn && !settings.Schedule.IsOnAt(now)) { if (state.IsSwitchedOn) { setSwitchedOn = false; setMarker = true; } return; } if (!state.IsSwitchedOn && (state.SwitchedOffByInferno || ignoreRain)) { setSwitchedOn = true; } if (state.SwitchedOffByInferno) { setMarker = false; } } private static float? ScaleFuelLoss(double multiplier, float maxFuel, float fuel, float? baseline) { if (baseline.HasValue) { float valueOrDefault = baseline.GetValueOrDefault(); if (!(fuel >= valueOrDefault)) { float num = valueOrDefault - fuel; float num2 = (float)Math.Max(0.0, Math.Min(maxFuel, (double)valueOrDefault - (double)num * multiplier)); if (!(Math.Abs(num2 - fuel) > 0.0001f)) { return null; } return num2; } } return null; } private static float? BurnUnattended(float fuel, double seconds, float secondsPerFuel, double multiplier) { double num = seconds / (double)secondsPerFuel * multiplier; float num2 = (float)Math.Max(0.0, (double)fuel - num); if (!(Math.Abs(num2 - fuel) > 0.0001f)) { return null; } return num2; } } public readonly struct FuelDecision { public float? SetFuel { get; } public bool? SetSwitchedOn { get; } public bool? SetSwitchedOffByInferno { get; } public bool RefreshBurnClock { get; } public float Baseline { get; } public bool MustWin { get; } public bool HasChanges { get { if (!SetFuel.HasValue && !SetSwitchedOn.HasValue && !SetSwitchedOffByInferno.HasValue) { return RefreshBurnClock; } return true; } } public FuelDecision(float? setFuel, bool? setSwitchedOn, bool? setSwitchedOffByInferno, bool refreshBurnClock, float baseline, bool mustWin = false) { SetFuel = setFuel; SetSwitchedOn = setSwitchedOn; SetSwitchedOffByInferno = setSwitchedOffByInferno; RefreshBurnClock = refreshBurnClock; Baseline = baseline; MustWin = mustWin; } } public sealed class FuelItemInfo { public float MaxFuel { get; } public bool HasOnOffSwitch { get; } public float SecondsPerFuel { get; } public bool BurnsOverTime { get { if (HasOnOffSwitch) { return SecondsPerFuel > 0f; } return false; } } public FuelItemInfo(float maxFuel, bool hasOnOffSwitch, float secondsPerFuel = 0f) { if (!(maxFuel > 0f) || float.IsInfinity(maxFuel)) { throw new ArgumentOutOfRangeException("maxFuel", maxFuel, "Max fuel must be a finite number > 0."); } if (!(secondsPerFuel >= 0f) || float.IsInfinity(secondsPerFuel)) { throw new ArgumentOutOfRangeException("secondsPerFuel", secondsPerFuel, "Seconds per fuel must be a finite number ≥ 0."); } MaxFuel = maxFuel; HasOnOffSwitch = hasOnOffSwitch; SecondsPerFuel = secondsPerFuel; } } public readonly struct FuelState { public float Fuel { get; } public bool IsSwitchedOn { get; } public bool SwitchedOffByInferno { get; } public bool IsUnattended { get; } public double SecondsSinceBurnClock { get; } public FuelState(float fuel, bool isSwitchedOn, bool switchedOffByInferno, bool isUnattended, double secondsSinceBurnClock = 0.0) { Fuel = fuel; IsSwitchedOn = isSwitchedOn; SwitchedOffByInferno = switchedOffByInferno; IsUnattended = isUnattended; SecondsSinceBurnClock = secondsSinceBurnClock; } } } namespace Inferno.Core.Diagnostics { public readonly struct ErrorLogDecision { public bool Log { get; } public bool FirstTime { get; } public int Suppressed { get; } internal ErrorLogDecision(bool log, bool firstTime, int suppressed) { Log = log; FirstTime = firstTime; Suppressed = suppressed; } } public sealed class ErrorThrottle { private sealed class Entry { public double LastLogged { get; set; } public int Suppressed { get; set; } } private readonly double _intervalSeconds; private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(StringComparer.Ordinal); public int Total { get; private set; } public int Distinct => _entries.Count; public ErrorThrottle(double intervalSeconds) { if (!(intervalSeconds > 0.0) || double.IsInfinity(intervalSeconds)) { throw new ArgumentOutOfRangeException("intervalSeconds", intervalSeconds, "Interval must be a finite number > 0."); } _intervalSeconds = intervalSeconds; } public ErrorLogDecision Record(string key, double nowSeconds) { if (key == null) { throw new ArgumentNullException("key"); } Total++; if (!_entries.TryGetValue(key, out Entry value)) { _entries[key] = new Entry { LastLogged = nowSeconds }; return new ErrorLogDecision(log: true, firstTime: true, 0); } if (nowSeconds - value.LastLogged < _intervalSeconds) { value.Suppressed++; return new ErrorLogDecision(log: false, firstTime: false, value.Suppressed); } int suppressed = value.Suppressed; value.Suppressed = 0; value.LastLogged = nowSeconds; return new ErrorLogDecision(log: true, firstTime: false, suppressed); } } public sealed class RunningStats { private double _total; public int Count { get; private set; } public double Max { get; private set; } public double Average { get { if (Count != 0) { return _total / (double)Count; } return 0.0; } } public void Add(double milliseconds) { if (milliseconds >= 0.0 && !double.IsInfinity(milliseconds)) { Count++; _total += milliseconds; Max = Math.Max(Max, milliseconds); } } public void Reset() { Count = 0; _total = 0.0; Max = 0.0; } public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0} × avg {1:0.00} ms, max {2:0.00} ms", Count, Average, Max); } } } namespace Inferno.Core.Commands { public sealed class CommandExecutor { private readonly struct NearbyScope { public string? Filter { get; } public float? Radius { get; } public NearbyScope(string? filter, float? radius) { Filter = filter; Radius = radius; } public override string ToString() { string text = ((Filter == null) ? "nearby" : (Filter + " nearby")); float? radius = Radius; if (!radius.HasValue) { return text; } return text + " " + radius.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); } } private sealed class UndoEntry { public string Description { get; } public IReadOnlyList<KeyValuePair<string, ItemSettings>> Items { get; } public IReadOnlyList<KeyValuePair<string, ItemSettings?>> Objects { get; } public string? GeneralSetting { get; private set; } public Func<GeneralSettings, bool, GeneralSettings>? WriteGeneral { get; private set; } public bool GeneralValue { get; private set; } private UndoEntry(string description, IReadOnlyList<KeyValuePair<string, ItemSettings>> items, IReadOnlyList<KeyValuePair<string, ItemSettings?>> objects) { Description = description; Items = items; Objects = objects; } public static UndoEntry ForItems(string description, IReadOnlyList<KeyValuePair<string, ItemSettings>> before) { return new UndoEntry(description, before, Array.Empty<KeyValuePair<string, ItemSettings>>()); } public static UndoEntry ForObjects(string description, IReadOnlyList<KeyValuePair<string, ItemSettings?>> before) { return new UndoEntry(description, Array.Empty<KeyValuePair<string, ItemSettings>>(), before); } public static UndoEntry ForGeneral(string description, string setting, Func<GeneralSettings, bool, GeneralSettings> write, bool before) { return new UndoEntry(description, Array.Empty<KeyValuePair<string, ItemSettings>>(), Array.Empty<KeyValuePair<string, ItemSettings>>()) { GeneralSetting = setting, WriteGeneral = write, GeneralValue = before }; } } public const string GeneralSection = "General"; public const int UndoDepth = 10; private const int MaxNamesInReply = 4; private static readonly string[] HelpLines = new string[6] { "Inferno: write !fires <command> on a sign or in chat.", "alwayson <item> on|off – never needs fuel", "burnrate <item> -10..10 – fuel use, 0 = normal", "schedule <item> night|day|off – when it's lit", "preset night|eternal|vanilla – one-step setups", "More: help items · help presets · help admin · status" }; private static readonly Dictionary<string, string[]> HelpTopics = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase) { ["items"] = new string[6] { "<item> can be:", "a name as in game – hot tub, standing wood torch", "a word – torches, braziers, fires, lanterns", "a group – lights, stations, all", "show <item> – its settings · list – everything", "add 'nearby' (ward / 20 m) or 'nearby 5' (metres)" }, ["presets"] = new string[6] { "preset eternal [item] – always on (the default)", "preset night [item] – lit 18:00-06:00, no fuel used", "preset vanilla [item] – normal game behaviour", "schedule <item> 18:00 19:00 – your own times", "undo – take back your last change", "reset <item> – back to defaults" }, ["admin"] = new string[5] { "adminonly on|off – only admins may change settings", "hidecommands on|off – hide !fires lines in chat", "serverownership on|off – experimental, see readme", "smoke <item> on|off – client mod players only", "F5 console: listkeys fires <command>" } }; private readonly ItemCatalog _catalog; private readonly ISettingsStore _store; private readonly Func<IReadOnlyList<string>> _status; private readonly Dictionary<string, List<UndoEntry>> _undo = new Dictionary<string, List<UndoEntry>>(StringComparer.Ordinal); private Func<float?, NearbySelection>? _nearby; public const string NearbyWord = "nearby"; public CommandExecutor(ItemCatalog catalog, ISettingsStore store, Func<IReadOnlyList<string>>? status = null) { _catalog = catalog ?? throw new ArgumentNullException("catalog"); _store = store ?? throw new ArgumentNullException("store"); _status = status ?? ((Func<IReadOnlyList<string>>)(() => new <>z__ReadOnlySingleElementList<string>("Inferno is running."))); } public CommandResult Execute(ParsedCommand command, CommandSender sender, Func<float?, NearbySelection>? nearby = null) { _nearby = nearby; if (command == null) { throw new ArgumentNullException("command"); } if (sender == null) { throw new ArgumentNullException("sender"); } switch (command.Kind) { case CommandKind.Help: return Help(command.Name); case CommandKind.Status: return Ok(_status()); case CommandKind.List: case CommandKind.Show: { if (!TrySplitNearby(command.Target, out var scope2)) { return Show(command.Target); } return ShowNearby(scope2); } default: { if (!PermissionPolicy.CanChangeSettings(sender.IsAdmin, _store.General.AdminOnly)) { return new CommandResult(new <>z__ReadOnlySingleElementList<string>("Only admins can change Inferno settings while AdminOnly is on."), Array.Empty<SettingChange>(), denied: true); } NearbyScope scope; return command.Kind switch { CommandKind.AlwaysOn => UpdateItems(sender, command.Target, "AlwaysOn", OnOff(command.Flag), (CatalogItem _, ItemSettings s) => s.WithAlwaysOn(command.Flag)), CommandKind.Smoke => UpdateItems(sender, command.Target, "Smoke", OnOff(command.Flag), (CatalogItem _, ItemSettings s) => s.WithSmoke(command.Flag)), CommandKind.BurnRate => UpdateItems(sender, command.Target, "BurnRate", Number(command.Number), (CatalogItem _, ItemSettings s) => s.WithBurnRate(command.Number).WithAlwaysOn(value: false)), CommandKind.Schedule => UpdateItems(sender, command.Target, "Schedule", command.Schedule.ToString(), (CatalogItem i, ItemSettings s) => i.CanSchedule ? ((!command.Schedule.IsAlwaysOn) ? s.WithSchedule(command.Schedule).WithAlwaysOn(value: false) : s.WithSchedule(command.Schedule)) : null), CommandKind.Reset => TrySplitNearby(command.Target, out scope) ? UpdateObjects(sender, scope, "Settings", "item type's", null) : UpdateItems(sender, command.Target, "Settings", "defaults", (CatalogItem i, ItemSettings _) => ItemSettings.DefaultFor(i.Kind)), CommandKind.Preset => ApplyPreset(sender, command), CommandKind.Undo => Undo(sender), CommandKind.AdminOnly => UpdateGeneral(sender, "AdminOnly", (GeneralSettings g) => g.AdminOnly, (GeneralSettings g, bool v) => g.WithAdminOnly(v), command.Flag), CommandKind.HideCommands => UpdateGeneral(sender, "HideCommands", (GeneralSettings g) => g.HideCommands, (GeneralSettings g, bool v) => g.WithHideCommands(v), command.Flag), CommandKind.IgnoreRain => UpdateGeneral(sender, "IgnoreRain", (GeneralSettings g) => g.IgnoreRain, (GeneralSettings g, bool v) => g.WithIgnoreRain(v), command.Flag), _ => UpdateGeneral(sender, "ServerOwnership", (GeneralSettings g) => g.ServerOwnership, (GeneralSettings g, bool v) => g.WithServerOwnership(v), command.Flag), }; } } } public string Describe(CatalogItem item) { if (item == null) { throw new ArgumentNullException("item"); } ItemSettings item2 = _store.GetItem(item.PrefabName); string text = ((item.Kind == ItemKind.LightSource) ? "light" : "station"); string text2 = (item.CanSchedule ? item2.Schedule.ToString() : "n/a"); return item.DisplayName + " (" + item.PrefabName + ", " + text + "): alwayson=" + OnOff(item2.AlwaysOn) + " burnrate=" + Number(item2.BurnRateLevel) + " schedule=" + text2 + " smoke=" + OnOff(item2.Smoke); } private static CommandResult Help(string? topic) { if (topic == null) { return Ok(HelpLines); } if (!HelpTopics.TryGetValue(topic, out string[] value)) { return Ok(new <>z__ReadOnlySingleElementList<string>("No help topic '" + topic + "'. Topics: items, presets, admin.")); } return Ok(value); } private CommandResult Show(string target) { if (!TryResolve(target, out IReadOnlyList<CatalogItem> items, out string error)) { return Ok(new <>z__ReadOnlySingleElementList<string>(error)); } List<string> list = new List<string>(items.Count); foreach (CatalogItem item in items) { list.Add(Describe(item)); } return Ok(list); } private CommandResult ApplyPreset(CommandSender sender, ParsedCommand command) { if (!Presets.TryGet(command.Name, out Preset preset)) { string text = string.Join(", ", Presets.All.Select((Preset p) => p.Name + " (" + p.Description + ")")); return Ok(new <>z__ReadOnlySingleElementList<string>("Unknown preset '" + command.Name + "'. Presets: " + text + ".")); } string text2 = command.Target ?? preset.DefaultTarget; if (TrySplitNearby(text2, out var scope) && scope.Filter == null && preset.DefaultTarget != "all") { text2 = preset.DefaultTarget + " " + text2; } return UpdateItems(sender, text2, "Preset", preset.Name, preset.Apply); } private CommandResult UpdateItems(CommandSender sender, string target, string settingName, string newValueText, Func<CatalogItem, ItemSettings, ItemSettings?> update) { if (TrySplitNearby(target, out var scope)) { return UpdateObjects(sender, scope, settingName, newValueText, update); } if (!TryResolve(target, out IReadOnlyList<CatalogItem> items, out string error)) { return Ok(new <>z__ReadOnlySingleElementList<string>(error)); } List<SettingChange> list = new List<SettingChange>(); List<KeyValuePair<string, ItemSettings>> list2 = new List<KeyValuePair<string, ItemSettings>>(); List<string> list3 = new List<string>(); int num = 0; int num2 = 0; foreach (CatalogItem item2 in items) { ItemSettings item = _store.GetItem(item2.PrefabName); ItemSettings itemSettings = update(item2, item); if (itemSettings == null) { num++; } else if (!itemSettings.Equals(item)) { if (item.AlwaysOn && !itemSettings.AlwaysOn && settingName != "AlwaysOn" && settingName != "Preset" && settingName != "Settings") { num2++; } _store.SetItem(item2.PrefabName, itemSettings); list.Add(new SettingChange(item2.PrefabName, settingName, item.ToString(), itemSettings.ToString())); list2.Add(new KeyValuePair<string, ItemSettings>(item2.PrefabName, item)); list3.Add(item2.DisplayName); } } List<string> list4 = new List<string>(); list4.Add($"{settingName} = {newValueText}: {list.Count} of {items.Count} item(s) changed."); List<string> list5 = list4; if (list3.Count > 1 && !IsFixedGroup(target)) { list5.Add("Changed: " + NameList(list3)); } if (num2 > 0) { list5.Add($"AlwaysOn turned off for {num2} item(s) so this takes effect."); } if (num > 0) { list5.Add($"{num} item(s) skipped: they have no on/off switch and can't follow a schedule."); } if (list.Count > 0) { Remember(sender, UndoEntry.ForItems(settingName + " = " + newValueText + " on '" + target + "'", list2)); } return new CommandResult(list5, list, denied: false); } private CommandResult UpdateGeneral(CommandSender sender, string settingName, Func<GeneralSettings, bool> read, Func<GeneralSettings, bool, GeneralSettings> write, bool newValue) { GeneralSettings general = _store.General; if (read(general) == newValue) { return Ok(new <>z__ReadOnlySingleElementList<string>(settingName + " is already " + OnOff(newValue) + ".")); } _store.General = write(general, newValue); Remember(sender, UndoEntry.ForGeneral(settingName + " = " + OnOff(newValue), settingName, write, !newValue)); SettingChange item = new SettingChange("General", settingName, OnOff(!newValue), OnOff(newValue)); return new CommandResult(new <>z__ReadOnlySingleElementList<string>(settingName + " = " + OnOff(newValue) + "."), new <>z__ReadOnlySingleElementList<SettingChange>(item), denied: false); } private CommandResult Undo(CommandSender sender) { if (!_undo.TryGetValue(sender.PlatformId, out List<UndoEntry> value) || value.Count == 0) { return Ok(new <>z__ReadOnlySingleElementList<string>("Nothing to undo.")); } UndoEntry undoEntry = value[value.Count - 1]; value.RemoveAt(value.Count - 1); List<SettingChange> list = new List<SettingChange>(); string generalSetting = undoEntry.GeneralSetting; if (generalSetting != null) { GeneralSettings general = _store.General; _store.General = undoEntry.WriteGeneral(general, undoEntry.GeneralValue); list.Add(new SettingChange("General", generalSetting, OnOff(!undoEntry.GeneralValue), OnOff(undoEntry.GeneralValue))); } foreach (KeyValuePair<string, ItemSettings> item2 in undoEntry.Items) { ItemSettings item = _store.GetItem(item2.Key); if (!item.Equals(item2.Value)) { _store.SetItem(item2.Key, item2.Value); list.Add(new SettingChange(item2.Key, "Undo", item.ToString(), item2.Value.ToString())); } } foreach (KeyValuePair<string, ItemSettings> @object in undoEntry.Objects) { ItemSettings itemSettings = _store.GetObject(@object.Key); if (!object.Equals(itemSettings, @object.Value)) { _store.SetObject(@object.Key, @object.Value); list.Add(new SettingChange(@object.Key, "Undo", OwnText(itemSettings), OwnText(@object.Value))); } } string text = ((value.Count == 0) ? string.Empty : $" ({value.Count} more to undo)"); return new CommandResult(new <>z__ReadOnlySingleElementList<string>("Undone: " + undoEntry.Description + "." + text), list, denied: false); } private void Remember(CommandSender sender, UndoEntry entry) { if (!_undo.TryGetValue(sender.PlatformId, out List<UndoEntry> value)) { value = new List<UndoEntry>(); _undo[sender.PlatformId] = value; } value.Add(entry); if (value.Count > 10) { value.RemoveAt(0); } } private static bool TrySplitNearby(string target, out NearbyScope scope) { scope = default(NearbyScope); string[] array = target.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int num = Array.FindLastIndex(array, (string w) => string.Equals(w, "nearby", StringComparison.OrdinalIgnoreCase)); if (num < 0 || num < array.Length - 2) { return false; } float? radius = null; if (num == array.Length - 2) { if (!float.TryParse(array[^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return false; } radius = result; } scope = new NearbyScope((num == 0) ? null : string.Join(" ", array, 0, num), radius); return true; } private bool TrySelectNearby(NearbyScope scope, out List<(NearbyObject Object, CatalogItem Item)> selected, out string area, out string error) { selected = new List<(NearbyObject, CatalogItem)>(); area = string.Empty; error = string.Empty; string filter = scope.Filter; if (_nearby == null) { error = "'nearby' only works on a sign, in chat or in the F5 console."; return false; } float? radius = scope.Radius; if (radius.HasValue) { float valueOrDefault = radius.GetValueOrDefault(); if (!(valueOrDefault >= 1f) || !(valueOrDefault <= 100f)) { error = string.Format(CultureInfo.InvariantCulture, "Radius must be {0:0} to {1:0} m.", 1f, 100f); return false; } } HashSet<string> hashSet = null; if (filter != null) { if (!TryResolve(filter, out IReadOnlyList<CatalogItem> items, out error)) { return false; } hashSet = new HashSet<string>(items.Select((CatalogItem i) => i.PrefabName), StringComparer.OrdinalIgnoreCase); } NearbySelection nearbySelection = _nearby(scope.Radius); area = nearbySelection.AreaDescription; foreach (NearbyObject @object in nearbySelection.Objects) { if ((hashSet == null || hashSet.Contains(@object.PrefabName)) && _catalog.TryGet(@object.PrefabName, out CatalogItem item)) { selected.Add((@object, item)); } } if (selected.Count == 0) { error = "No " + ((filter == null) ? "fires" : ("'" + filter + "'")) + " " + area + "."; return false; } return true; } private CommandResult UpdateObjects(CommandSender sender, NearbyScope scope, string settingName, string newValueText, Func<CatalogItem, ItemSettings, ItemSettings?>? update) { if (!TrySelectNearby(scope, out List<(NearbyObject, CatalogItem)> selected, out string area, out string error)) { return Ok(new <>z__ReadOnlySingleElementList<string>(error)); } List<SettingChange> list = new List<SettingChange>(); List<KeyValuePair<string, ItemSettings>> list2 = new List<KeyValuePair<string, ItemSettings>>(); int num = 0; int num2 = 0; int num3 = 0; foreach (var (nearbyObject, catalogItem) in selected) { if (!nearbyObject.Allowed) { num++; continue; } ItemSettings itemSettings = _store.GetObject(nearbyObject.Key); ItemSettings itemSettings2 = itemSettings ?? _store.GetItem(catalogItem.PrefabName); ItemSettings itemSettings3; if (update == null) { if (itemSettings == null) { continue; } itemSettings3 = null; } else { itemSettings3 = update(catalogItem, itemSettings2); if (itemSettings3 == null) { num2++; continue; } if (itemSettings3.Equals(itemSettings2)) { continue; } if (itemSettings2.AlwaysOn && !itemSettings3.AlwaysOn && settingName != "AlwaysOn" && settingName != "Preset") { num3++; } } _store.SetObject(nearbyObject.Key, itemSettings3); list2.Add(new KeyValuePair<string, ItemSettings>(nearbyObject.Key, itemSettings)); list.Add(new SettingChange(catalogItem.PrefabName + " " + nearbyObject.Key, settingName, OwnText(itemSettings), OwnText(itemSettings3))); } List<string> list3 = new List<string>(); list3.Add((update == null) ? $"{list.Count} of {selected.Count} fire(s) {area} back to their item type's settings." : $"{settingName} = {newValueText}: {list.Count} of {selected.Count} fire(s) {area} changed."); List<string> list4 = list3; if (num3 > 0) { list4.Add($"AlwaysOn turned off for {num3} of them so this takes effect."); } if (num > 0) { list4.Add($"{num} skipped: they're in a ward you have no access to."); } if (num2 > 0) { list4.Add($"{num2} skipped: no on/off switch, so no schedule."); } if (list.Count > 0) { Remember(sender, UndoEntry.ForObjects($"{settingName} = {newValueText} on '{scope}'", list2)); } return new CommandResult(list4, list, denied: false); } private CommandResult ShowNearby(NearbyScope scope) { if (!TrySelectNearby(scope, out List<(NearbyObject, CatalogItem)> selected, out string area, out string error)) { return Ok(new <>z__ReadOnlySingleElementList<string>(error)); } List<string> list = new List<string> { $"{selected.Count} fire(s) {area}:" }; foreach (IGrouping<(string, string, bool), (CatalogItem, ItemSettings, bool)> item in (from s in selected select (Item: s.Item, Own: _store.GetObject(s.Object.Key)) into s select (Item: s.Item, Settings: s.Own ?? _store.GetItem(s.Item.PrefabName), IsOwn: s.Own != null) into s group s by (DisplayName: s.Item.DisplayName, Text: s.Settings.ToString(), IsOwn: s.IsOwn)).OrderBy<IGrouping<(string, string, bool), (CatalogItem, ItemSettings, bool)>, string>((IGrouping<(string DisplayName, string Text, bool IsOwn), (CatalogItem Item, ItemSettings Settings, bool IsOwn)> g) => g.Key.DisplayName, StringComparer.OrdinalIgnoreCase)) { string text = (item.Key.Item3 ? " (own settings)" : string.Empty); list.Add($"{item.Key.Item1} x{item.Count()}{text}: {item.Key.Item2}"); } return Ok(list); } private bool TryResolve(string target, out IReadOnlyList<CatalogItem> items, out string error) { error = string.Empty; if (!_catalog.TryResolve(target, out items)) { IReadOnlyList<string> readOnlyList = _catalog.Suggest(target); error = ((readOnlyList.Count > 0) ? ("Unknown item '" + target + "'. Did you mean: " + string.Join(", ", readOnlyList) + "?") : ("Unknown item '" + target + "'. Use 'list' to see all items.")); return false; } if (items.Count == 0) { error = "No items in '" + target + "'."; return false; } return true; } private static bool IsFixedGroup(string target) { if (!string.Equals(target, "all", StringComparison.OrdinalIgnoreCase) && !string.Equals(target, "lights", StringComparison.OrdinalIgnoreCase)) { return string.Equals(target, "stations", StringComparison.OrdinalIgnoreCase); } return true; } private static string NameList(List<string> names) { List<string> list = names.Distinct<string>(StringComparer.OrdinalIgnoreCase).ToList(); string text = string.Join(", ", list.Take(4)); if (list.Count <= 4) { return text; } return $"{text}, +{list.Count - 4} more"; } private static string OwnText(ItemSettings? settings) { if (settings != null) { return settings.ToString(); } return "follows item type"; } private static CommandResult Ok(IReadOnlyList<string> reply) { return new CommandResult(reply, Array.Empty<SettingChange>(), denied: false); } private static string OnOff(bool value) { if (!value) { return "off"; } return "on"; } private static string Number(int value) { return value.ToString(CultureInfo.InvariantCulture); } } public enum CommandKind { Help, Status, List, Show, AlwaysOn, BurnRate, Schedule, Smoke, Reset, Preset, Undo, AdminOnly, HideCommands, IgnoreRain, ServerOwnership } public enum ParseOutcome { NotACommand, Command, Invalid } public static class CommandParser { public const string Prefix = "!fires"; public const string ConsolePrefix = "listkeys fires"; public const int MaxLength = 200; private const string ConsoleCarrier = "listkeys"; private const string ConsoleKeyword = "fires"; private static readonly char[] Whitespace = new char[2] { ' ', '\t' }; private static readonly string[] Subcommands = new string[15] { "HELP", "STATUS", "LIST", "SHOW", "RESET", "ALWAYSON", "SMOKE", "BURNRATE", "SCHEDULE", "PRESET", "UNDO", "ADMINONLY", "HIDECOMMANDS", "IGNORERAIN", "SERVEROWNERSHIP" }; public static ParseOutcome Parse(string? text, out ParsedCommand command, out string error) { command = null; error = string.Empty; text = RichText.Strip(text); string[] array = text.Split(Whitespace, StringSplitOptions.RemoveEmptyEntries); string prefix; if (array.Length >= 1 && string.Equals(array[0], "!fires", StringComparison.OrdinalIgnoreCase)) { prefix = "!fires"; } else { if (array.Length < 2 || !string.Equals(array[0], "listkeys", StringComparison.OrdinalIgnoreCase) || !string.Equals(array[1], "fires", StringComparison.OrdinalIgnoreCase)) { return ParseOutcome.NotACommand; } prefix = "listkeys fires"; array = array.Skip(1).ToArray(); } if (text.Length > 200) { error = $"Command too long (max {200} characters)."; return ParseOutcome.Invalid; } if (array.Length == 1) { command = new ParsedCommand(CommandKind.Help); return ParseOutcome.Command; } ParsedCommand parsedCommand = ParseSubcommand(array, prefix, out error); if (parsedCommand == null) { return ParseOutcome.Invalid; } command = parsedCommand; return ParseOutcome.Command; } private static ParsedCommand? ParseSubcommand(string[] t, string prefix, out string error) { error = string.Empty; switch (t[1].ToUpperInvariant()) { case "HELP": if (t.Length == 2) { return new ParsedCommand(CommandKind.Help); } if (t.Length == 3) { string name = t[2]; return new ParsedCommand(CommandKind.Help, null, flag: false, 0, default(DailySchedule), name); } break; case "STATUS": if (t.Length == 2) { return new ParsedCommand(CommandKind.Status); } break; case "LIST": if (t.Length == 2) { return new ParsedCommand(CommandKind.List, "all"); } if (t.Length >= 3) { return new ParsedCommand(CommandKind.List, Item(t, 0)); } break; case "SHOW": if (t.Length >= 3) { return new ParsedCommand(CommandKind.Show, Item(t, 0)); } break; case "RESET": if (t.Length >= 3) { return new ParsedCommand(CommandKind.Reset, Item(t, 0)); } break; case "ALWAYSON": { if (t.Length >= 4 && TryParseOnOff(t[^1], out var value2)) { return new ParsedCommand(CommandKind.AlwaysOn, Item(t, 1), value2); } break; } case "SMOKE": { if (t.Length >= 4 && TryParseOnOff(t[^1], out var value5)) { return new ParsedCommand(CommandKind.Smoke, Item(t, 1), value5); } break; } case "BURNRATE": { if (t.Length >= 4 && TryParseBurnRate(t[^1], out var level)) { return new ParsedCommand(CommandKind.BurnRate, Item(t, 1), flag: false, level); } break; } case "SCHEDULE": { if (t.Length >= 4 && TryParseScheduleWord(t[^1], out var schedule)) { return new ParsedCommand(CommandKind.Schedule, Item(t, 1), flag: false, 0, schedule); } if (t.Length >= 5 && ClockSetting.TryParse(t[^2], out var result) && ClockSetting.TryParse(t[^1], out var result2)) { return new ParsedCommand(CommandKind.Schedule, Item(t, 2), flag: false, 0, new DailySchedule(result, result2)); } break; } case "PRESET": if (t.Length >= 3) { string target = ((t.Length > 3) ? Item(t, 0, 3) : null); string name = t[2]; return new ParsedCommand(CommandKind.Preset, target, flag: false, 0, default(DailySchedule), name); } break; case "UNDO": if (t.Length == 2) { return new ParsedCommand(CommandKind.Undo); } break; case "ADMINONLY": { if (t.Length == 3 && TryParseOnOff(t[2], out var value6)) { return new ParsedCommand(CommandKind.AdminOnly, null, value6); } break; } case "HIDECOMMANDS": { if (t.Length == 3 && TryParseOnOff(t[2], out var value4)) { return new ParsedCommand(CommandKind.HideCommands, null, value4); } break; } case "IGNORERAIN": { if (t.Length == 3 && TryParseOnOff(t[2], out var value3)) { return new ParsedCommand(CommandKind.IgnoreRain, null, value3); } break; } case "SERVEROWNERSHIP": { if (t.Length == 3 && TryParseOnOff(t[2], out var value)) { return new ParsedCommand(CommandKind.ServerOwnership, null, value); } break; } } error = UsageFor(t[1], prefix); if (!IsSubcommand(t[1])) { string text = SuggestOrder(t, prefix); if (text != null) { error = "Did you mean: " + text; } } return null; } private static bool IsSubcommand(string word) { return Array.IndexOf(Subcommands, word.ToUpperInvariant()) >= 0; } private static string? SuggestOrder(string[] t, string prefix) { for (int i = 2; i < t.Length; i++) { if (IsSubcommand(t[i])) { List<string> second = t.Skip(1).Take(i - 1).ToList(); List<string> list = t.Skip(i + 1).ToList(); IEnumerable<string> values = ((string.Equals(t[i], "preset", StringComparison.OrdinalIgnoreCase) && list.Count > 0) ? new string[2] { t[i], list[0] }.Concat(second).Concat(list.Skip(1)) : new string[1] { t[i] }.Concat(second).Concat(list)); string text = prefix + " " + string.Join(" ", values); if (Parse(text, out ParsedCommand _, out string _) != ParseOutcome.Command) { return null; } return text; } } return null; } internal static string UsageFor(string subcommand, string prefix) { return subcommand.ToUpperInvariant() switch { "HELP" => "Usage: " + prefix + " help [items|presets|admin]", "STATUS" => "Usage: " + prefix + " status", "SHOW" => "Usage: " + prefix + " show <item> (item = name as in game, e.g. hot tub; or all, lights, stations)", "RESET" => "Usage: " + prefix + " reset <item>", "ALWAYSON" => "Usage: " + prefix + " alwayson <item> on|off", "SMOKE" => "Usage: " + prefix + " smoke <item> on|off (seen only by players with the Inferno client mod)", "BURNRATE" => $"Usage: {prefix} burnrate <item> <{-10}..{10}> (0 = vanilla, each step 10 %)", "SCHEDULE" => "Usage: " + prefix + " schedule <item> <on HH:MM> <off HH:MM> or " + prefix + " schedule <item> night|day|off", "PRESET" => "Usage: " + prefix + " preset eternal|night|vanilla [item]", "UNDO" => "Usage: " + prefix + " undo", "ADMINONLY" => "Usage: " + prefix + " adminonly on|off", "HIDECOMMANDS" => "Usage: " + prefix + " hidecommands on|off", "IGNORERAIN" => "Usage: " + prefix + " ignorerain on|off", "SERVEROWNERSHIP" => "Usage: " + prefix + " serverownership on|off", _ => "Unknown command '" + subcommand + "'. Type " + prefix + " help", }; } private static string Item(string[] tokens, int trailingArguments, int first = 2) { return string.Join(" ", tokens, first, tokens.Length - first - trailingArguments); } private static bool TryParseScheduleWord(string text, out DailySchedule schedule) { switch (text.ToUpperInvariant()) { case "OFF": schedule = DailySchedule.AlwaysOn; return true; case "NIGHT": schedule = DailySchedule.Night; return true; case "DAY": schedule = DailySchedule.Day; return true; default: schedule = default(DailySchedule); return false; } } private static bool TryParseOnOff(string text, out bool value) { value = string.Equals(text, "on", StringComparison.OrdinalIgnoreCase); if (!value) { return string.Equals(text, "off", StringComparison.OrdinalIgnoreCase); } return true; } private static bool TryParseBurnRate(string text, out int level) { level = 0; bool flag = text.StartsWith("-", StringComparison.Ordinal); string text2 = ((flag || text.StartsWith("+", StringComparison.Ordinal)) ? text.Substring(1) : text); int length = text2.Length; bool flag2 = ((length < 1 || length > 2) ? true : false); if (flag2 || !TimeOfDay.TryParseDigits(text2, out var value) || value > 10) { return false; } level = (flag ? (-value) : value); return true; } } public sealed class CommandResult { public IReadOnlyList<string> Reply { get; } public IReadOnlyList<SettingChange> Changes { get; } public bool Denied { get; } internal CommandResult(IReadOnlyList<string> reply, IReadOnlyList<SettingChange> changes, bool denied) { Reply = reply; Changes = changes; Denied = denied; } } public sealed class CommandSender { public string Name { get; } public string PlatformId { get; } public bool IsAdmin { get; } public CommandSender(string name, string platformId, bool isAdmin) { Name = name ?? throw new ArgumentNullException("name"); PlatformId = platformId ?? throw new ArgumentNullException("platformId"); IsAdmin = isAdmin; } public override string ToString() { return Name + " (" + PlatformId + (IsAdmin ? ", admin" : string.Empty) + ")"; } } public sealed class DuplicateFilter { private readonly double _windowSeconds; private readonly Dictionary<string, double> _lastSeen = new Dictionary<string, double>(); internal int Count => _lastSeen.Count; public DuplicateFilter(double windowSeconds) { if (!(windowSeconds > 0.0)) { throw new ArgumentOutOfRangeException("windowSeconds", windowSeconds, "Window must be > 0."); } _windowSeconds = windowSeconds; } public bool IsFirst(long senderId, string text, double nowSeconds) { Prune(nowSeconds); string key = senderId.ToString(CultureInfo.InvariantCulture) + "\n" + text; if (_lastSeen.ContainsKey(key)) { return false; } _lastSeen[key] = nowSeconds; return true; } private void Prune(double nowSeconds) { List<string> list = null; foreach (KeyValuePair<string, double> item in _lastSeen) { if (nowSeconds - item.Value >= _windowSeconds) { (list ?? (list = new List<string>())).Add(item.Key); } } if (list == null) { return; } foreach (string item2 in list) { _lastSeen.Remove(item2); } } } public sealed class ParsedCommand { public string? Name { get; } public CommandKind Kind { get; } public string? Target { get; } public bool Flag { get; } public int Number { get; } public DailySchedule Schedule { get; } internal ParsedCommand(CommandKind kind, string? target = null, bool flag = false, int number = 0, DailySchedule schedule = default(DailySchedule), string? name = null) { Kind = kind; Target = target; Flag = flag; Number = number; Schedule = schedule; Name = name; } } public enum RateDecision { Allow, DenyAndWarn, DenySilently } public sealed class RateLimiter { private sealed class Bucket { public double Tokens { get; set; } public double Updated { get; set; } public bool Warned { get; set; } } private readonly double _capacity; private readonly double _refillPerSecond; private readonly Dictionary<string, Bucket> _buckets = new Dictionary<string, Bucket>(StringComparer.Ordinal); public int Count => _buckets.Count; public RateLimiter(int burst, double refillPerSecond) { if (burst < 1) { throw new ArgumentOutOfRangeException("burst", burst, "Burst must be ≥ 1."); } if (!(refillPerSecond > 0.0) || double.IsInfinity(refillPerSecond)) { throw new ArgumentOutOfRangeException("refillPerSecond", refillPerSecond, "Refill rate must be a finite number > 0."); } _capacity = burst; _refillPerSecond = refillPerSecond; } public RateDecision TryTake(string playerKey, double nowSeconds) { if (playerKey == null) { throw new ArgumentNullException("playerKey"); } Prune(nowSeconds); if (!_buckets.TryGetValue(playerKey, out Bucket value)) { value = new Bucket { Tokens = _capacity, Updated = nowSeconds }; _buckets[playerKey] = value; } Refill(value, nowSeconds); if (value.Tokens >= 1.0) { value.Tokens -= 1.0; value.Warned = false; return RateDecision.Allow; } if (value.Warned) { return RateDecision.DenySilently; } value.Warned = true; return RateDecision.DenyAndWarn; } private void Refill(Bucket bucket, double now) { double num = Math.Max(0.0, now - bucket.Updated); bucket.Tokens = Math.Min(_capacity, bucket.Tokens + num * _refillPerSecond); bucket.Updated = now; } private void Prune(double now) { List<string> list = null; foreach (KeyValuePair<string, Bucket> bucket in _buckets) { Bucket value = bucket.Value; if (value.Tokens + (now - value.Updated) * _refillPerSecond >= _capacity) { (list ?? (list = new List<string>())).Add(bucket.Key); } } if (list == null) { return; } foreach (string item in list) { _buckets.Remove(item); } } } public static class ReplyLimiter { public static IReadOnlyList<string> Limit(IReadOnlyList<string> lines, int maxLines) { if (lines == null) { throw new ArgumentNullException("lines"); } if (maxLines < 2) { throw new ArgumentOutOfRangeException("maxLines", maxLines, "Need room for at least one line plus the note."); } if (lines.Count <= maxLines) { return lines; } int num = maxLines - 1; List<string> list = new List<string>(maxLines); for (int i = 0; i < num; i++) { list.Add(lines[i]); } list.Add(string.Format("… {0} more line(s). Use the F5 console ('{1} …') to see everything.", lines.Count - num, "listkeys fires")); return list; } } public static class RichText { private static readonly Regex Tag = new Regex("<[^<>]*>", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex LeadingTags = new Regex("^\\s*((?:<[^<>/][^<>]*>\\s*)+)", RegexOptions.Compiled | RegexOptions.CultureInvariant); public static string Strip(string? text) { if (text != null) { return Tag.Replace(text, string.Empty); } return string.Empty; } public static string Leading(string? text) { if (text == null) { return string.Empty; } Match match = LeadingTags.Match(text); if (!match.Success) { return string.Empty; } return match.Groups[1].Value.Trim(); } } public sealed class SettingChange { public string Section { get; } public string Setting { get; } public string OldValue { get; } public string NewValue { get; } public SettingChange(string section, string setting, string oldValue, string newValue) { Section = section; Setting = setting; OldValue = oldValue; NewValue = newValue; } public override string ToString() { return "[" + Section + "] " + Setting + ": " + OldValue + " -> " + NewValue; } } public static class SignReply { public const int MaxLength = 50; public static string Summarize(IReadOnlyList<string> reply) { if (reply == null) { throw new ArgumentNullException("reply"); } if (reply.Count == 0) { return string.Empty; } string text = reply[0].Trim(); if (text.Length > 50) { return text.Substring(0, 49) + "…"; } return text; } } } namespace Inferno.Core.Catalog { public sealed class CatalogItem { public string PrefabName { get; } public string DisplayName { get; } public ItemKind Kind { get; } public FuelItemInfo Fuel { get; } public bool CanSchedule => Fuel.HasOnOffSwitch; public CatalogItem(string prefabName, string displayName, ItemKind kind, FuelItemInfo fuel) { if (string.IsNullOrWhiteSpace(prefabName)) { throw new ArgumentException("Prefab name is required.", "prefabName"); } if (string.IsNullOrWhiteSpace(displayName)) { throw new ArgumentException("Display name is required.", "displayName"); } PrefabName = prefabName; DisplayName = displayName; Kind = kind; Fuel = fuel ?? throw new ArgumentNullException("fuel"); } } public sealed class ItemCatalog { public const string AllGroup = "all"; public const string LightsGroup = "lights"; public const string StationsGroup = "stations"; public const int MinKeywordLength = 3; private readonly Dictionary<string, CatalogItem> _byName = new Dictionary<string, CatalogItem>(StringComparer.OrdinalIgnoreCase); private readonly List<CatalogItem> _items = new List<CatalogItem>(); public IReadOnlyList<CatalogItem> Items => _items; public ItemCatalog(IEnumerable<CatalogItem> items) { if (items == null) { throw new ArgumentNullException("items"); } foreach (CatalogItem item in items) { if (item == null) { throw new ArgumentNullException("items", "Catalog items must not be null."); } if (IsGroupName(item.PrefabName)) { throw new ArgumentException("Prefab name '" + item.PrefabName + "' is reserved for a group.", "items"); } if (_byName.ContainsKey(item.PrefabName)) { throw new ArgumentException("Duplicate prefab '" + item.PrefabName + "'.", "items"); } _byName.Add(item.PrefabName, item); _items.Add(item); } _items.Sort((CatalogItem a, CatalogItem b) => StringComparer.OrdinalIgnoreCase.Compare(a.PrefabName, b.PrefabName)); } public bool TryGet(string prefabName, out CatalogItem item) { if (prefabName != null && _byName.TryGetValue(prefabName, out CatalogItem value)) { item = value; return true; } item = null; return false; } public bool TryResolve(string target, out IReadOnlyList<CatalogItem> items) { switch (target?.ToLowerInvariant()) { case "all": items = _items; return true; case "lights": items = _items.Where((CatalogItem i) => i.Kind == ItemKind.LightSource).ToList(); return true; case "stations": items = _items.Where((CatalogItem i) => i.Kind == ItemKind.FuelStation).ToList(); return true; default: { if (TryGet(target, out CatalogItem item)) { items = new <>z__ReadOnlySingleElementList<CatalogItem>(item); return true; } string key = Normalize(target); List<CatalogItem> list = ((key.Length == 0) ? new List<CatalogItem>() : _items.Where((CatalogItem i) => Normalize(i.DisplayName) == key).ToList()); if (list.Count > 0) { items = list; return true; } items = MatchKeyword(key); return items.Count > 0; } } } private List<CatalogItem> MatchKeyword(string key) { foreach (string stem in Stems(key)) { if (stem.Length >= 3) { List<CatalogItem> list = _items.Where((CatalogItem i) => Normalize(i.DisplayName).Contains(stem)).ToList(); if (list.Count > 0) { return list; } } } return new List<CatalogItem>(); } private static IEnumerable<string> Stems(string key) { yield return key; if (key.EndsWith("s", StringComparison.Ordinal)) { yield return key.Substring(0, key.Length - 1); } if (key.EndsWith("es", StringComparison.Ordinal)) { yield return key.Substring(0, key.Length - 2); } } public IReadOnlyList<string> Suggest(string target, int max = 3) { string key = Normalize(target); if (key.Length == 0) { return Array.Empty<string>(); } return _items.Select((CatalogItem i) => i.DisplayName).Distinct<string>(StringComparer.OrdinalIgnoreCase).Where(delegate(string name) { string text = Normalize(name); return text.Contains(key) || key.Contains(text); }) .OrderBy<string, string>((string name) => name, StringComparer.OrdinalIgnoreCase) .Take(max) .ToList(); } internal static string Normalize(string? text) { if (text == null) { return string.Empty; } char[] array = new char[text.Length]; int length = 0; foreach (char c in text) { if (char.IsLetterOrDigit(c)) { array[length++] = char.ToLowerInvariant(c); } } return new string(array, 0, length); } private static bool IsGroupName(string name) { if (!string.Equals(name, "all", StringComparison.OrdinalIgnoreCase) && !string.Equals(name, "lights", StringComparison.OrdinalIgnoreCase)) { return string.Equals(name, "stations", StringComparison.OrdinalIgnoreCase); } return true; } } } namespace Inferno.Core.Areas { public sealed class NearbyArea { public const float DefaultRadius = 20f; public const float MinRadius = 1f; public const float MaxRadius = 100f; private readonly IReadOnlyList<Ward> _wards; private readonly float _x; private readonly float _z; private readonly float _radius; public bool IsWardArea => _wards.Count > 0; public string Description { get { if (!IsWardArea) { return string.Format(CultureInfo.InvariantCulture, "within {0:0} m", _radius); } if (_wards.Count != 1) { return $"in the area of {_wards.Count} wards"; } return "in this ward's area"; } } private NearbyArea(IReadOnlyList<Ward> wards, float x, float z, float radius) { _wards = wards; _x = x; _z = z; _radius = radius; } public static NearbyArea Circle(float x, float z, float radius) { if (!(radius > 0f) || float.IsInfinity(radius)) { throw new ArgumentOutOfRangeException("radius", radius, "Radius must be a finite number > 0."); } return new NearbyArea(Array.Empty<Ward>(), x, z, radius); } public static NearbyArea Around(IEnumerable<Ward> wards, float x, float z, float fallbackRadius = 20f) { if (wards == null) { throw new ArgumentNullException("wards"); } if (!(fallbackRadius > 0f) || float.IsInfinity(fallbackRadius)) { throw new ArgumentOutOfRangeException("fallbackRadius", fallbackRadius, "Radius must be a finite number > 0."); } return new NearbyArea(wards.Where((Ward w) => w.Enabled && w.Covers(x, z)).ToList(), x, z, fallbackRadius); } public bool Contains(float x, float z) { if (IsWardArea) { return _wards.Any((Ward w) => w.Covers(x, z)); } float num = _x - x; float num2 = _z - z; return Math.Sqrt(num * num + num2 * num2) <= (double)_radius; } public static bool CanAccess(IEnumerable<Ward> wards, float x, float z, long playerId) { if (wards == null) { throw new ArgumentNullException("wards"); } bool flag = false; foreach (Ward ward in wards) { if (ward.Enabled && ward.Covers(x, z)) { if (ward.Grants(playerId)) { return true; } flag = true; } } return !flag; } } public sealed class NearbyObject { public string Key { get; } public string PrefabName { get; } public bool Allowed { get; } public NearbyObject(string key, string prefabName, bool allowed) { Key = key ?? throw new ArgumentNullException("key"); PrefabName = prefabName ?? throw new ArgumentNullException("prefabName"); Allowed = allowed; } } public sealed class NearbySelection { public IReadOnlyList<NearbyObject> Objects { get; } public string AreaDescription { get; } public NearbySelection(IReadOnlyList<NearbyObject> objects, string areaDescription) { Objects = objects ?? throw new ArgumentNullException("objects"); AreaDescription = areaDescription ?? throw new ArgumentNullException("areaDescription"); } } public sealed class Ward { public float X { get; } public float Z { get; } public float Radius { get; } public bool Enabled { get; } public long Creator { get; } public IReadOnlyCollection<long> Permitted { get; } public Ward(float x, float z, float radius, bool enabled, long creator, IReadOnlyCollection<long> permitted) { if (!(radius >= 0f) || float.IsInfinity(radius)) { throw new ArgumentOutOfRangeException("radius", radius, "Radius must be a finite number ≥ 0."); } X = x; Z = z; Radius = radius; Enabled = enabled; Creator = creator; Permitted = permitted ?? throw new ArgumentNullException("permitted"); } public bool Covers(float x, float z) { float num = X - x; float num2 = Z - z; return Math.Sqrt(num * num + num2 * num2) < (double)Radius; } public bool Grants(long playerId) { if (Creator == playerId) { return true; } foreach (long item in Permitted) { if (item == playerId) { return true; } } return false; } } }
plugins/Inferno.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Inferno.Core.Areas; using Inferno.Core.Catalog; using Inferno.Core.Commands; using Inferno.Core.Diagnostics; using Inferno.Core.Fuel; using Inferno.Core.Ownership; using Inferno.Core.Permissions; using Inferno.Core.Settings; using Inferno.Core.Sync; using Inferno.Core.Time; using Inferno.Game; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("GrundleLord")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 GrundleLord. MIT License.")] [assembly: AssemblyFileVersion("0.3.2.0")] [assembly: AssemblyInformationalVersion("0.3.2+4c3e115387ee3266bdea5a26431267084f7f433c")] [assembly: AssemblyProduct("Inferno")] [assembly: AssemblyTitle("Inferno")] [assembly: AssemblyVersion("0.3.2.0")] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlyArray<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => _items.Length; T IReadOnlyList<T>.this[int index] => _items[index]; int ICollection<T>.Count => _items.Length; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return ((IEnumerable<T>)_items).GetEnumerator(); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return ((ICollection<T>)_items).Contains(item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { ((ICollection<T>)_items).CopyTo(array, arrayIndex); } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { return ((IList<T>)_items).IndexOf(item); } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T> { object IEnumerator.Current => _item; T IEnumerator<T>.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => 1; T IReadOnlyList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection<T>.Count => 1; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer<T>.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer<T>.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return new Enumerator(_item); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return EqualityComparer<T>.Default.Equals(_item, item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { if (!EqualityComparer<T>.Default.Equals(_item, item)) { return -1; } return 0; } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } 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; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Inferno { [BepInPlugin("GrundleLord.Inferno", "Inferno", "0.3.2")] public sealed class InfernoPlugin : BaseUnityPlugin { private const string TestedGameVersion = "1.0.17"; private const float GiveUpAfterSeconds = 30f; private Harmony? _harmony; private float _failingSince = -1f; private ConfigSettingsStore? _store; private ClientSync? _client; private bool _clientFailed; private bool _failed; private void Awake() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) Diagnostics.Init(((BaseUnityPlugin)this).Logger); _store = new ConfigSettingsStore(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger); _client = new ClientSync(_store, ((BaseUnityPlugin)this).Logger); ClientFeatures.Store = _store; _harmony = new Harmony("GrundleLord.Inferno"); ApplyPatches(_harmony); string text = ((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Inferno 0.3.2 loaded (Valheim " + text + ").")); if (text != "1.0.17") { ((BaseUnityPlugin)this).Logger.LogWarning((object)("This Inferno build was tested on Valheim 1.0.17; this server runs " + text + ". It will probably work, but please report problems.")); } } private void Update() { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) TickClient(); if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer() || !((Object)(object)ZNetScene.instance != (Object)null) || ZDOMan.instance == null) { if (InfernoRuntime.Current != null) { InfernoRuntime.Stop(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"World unloaded; Inferno stopped."); } _failed = false; } else { if (_failed) { return; } if (InfernoRuntime.Current == null) { try { InfernoRuntime.Start(_store, ((BaseUnityPlugin)this).Logger, _harmony); } catch (Exception arg) { _failed = true; InfernoRuntime.Stop(); ((BaseUnityPlugin)this).Logger.LogError((object)string.Format("Inferno could not start and is disabled until the world is reloaded; the game runs as vanilla. Please report this with the log (Inferno {0}, Valheim {1}):\n{2}", "0.3.2", Version.CurrentVersion, arg)); return; } } float realtimeSinceStartup = Time.realtimeSinceStartup; try { InfernoRuntime.Current.Tick(realtimeSinceStartup); _failingSince = -1f; } catch (Exception e) { Diagnostics.Error("running Inferno's regular update", e); if (_failingSince < 0f) { _failingSince = realtimeSinceStartup; } else if (realtimeSinceStartup - _failingSince > 30f) { _failed = true; InfernoRuntime.Stop(); ((BaseUnityPlugin)this).Logger.LogError((object)$"Inferno kept failing for {30f:0} s and is disabled until the world is reloaded; the game runs as vanilla. See the first error above."); } } } } private void ApplyPatches(Harmony harmony) { List<Type> list = new List<Type>(typeof(GameHooks).GetNestedTypes(BindingFlags.NonPublic)); list.AddRange(typeof(ClientFeatures).GetNestedTypes(BindingFlags.NonPublic)); foreach (Type item in list) { try { harmony.CreateClassProcessor(item).Patch(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Could not install hook " + item.Name + "; that feature is disabled. Likely cause: a Valheim update. " + ex.Message)); } } } private void TickClient() { if (_clientFailed) { return; } try { _client.Tick(Time.realtimeSinceStartup); } catch (Exception e) { _clientFailed = true; Diagnostics.Error("running the client mod features (now disabled until the game restarts)", e); } } private void OnDestroy() { InfernoRuntime.Stop(); _store?.Dispose(); Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "GrundleLord.Inferno"; public const string PLUGIN_NAME = "Inferno"; public const string PLUGIN_VERSION = "0.3.2"; } } namespace Inferno.Game { internal static class ClientFeatures { [HarmonyPatch(typeof(SmokeSpawner), "Spawn")] private static class SmokePatch { private static readonly FieldRef<SmokeSpawner, float> LastSpawnTime = AccessTools.FieldRefAccess<SmokeSpawner, float>("m_lastSpawnTime"); private static readonly Func<SmokeSpawner, bool> TestBlocked = AccessTools.MethodDelegate<Func<SmokeSpawner, bool>>(AccessTools.Method(typeof(SmokeSpawner), "TestBlocked", (Type[])null, (Type[])null), (object)null, true); private static bool Prefix(SmokeSpawner __instance, float time) { if (!Active || SmokeWanted(__instance)) { return true; } if (!TestBlocked(__instance)) { LastSpawnTime.Invoke(__instance) = time; } return false; } private static bool SmokeWanted(SmokeSpawner spawner) { int instanceID = ((Object)spawner).GetInstanceID(); if (!ViewBySpawner.TryGetValue(instanceID, out ZNetView value)) { value = ((Component)spawner).GetComponentInParent<ZNetView>(); ViewBySpawner[instanceID] = value; } if ((Object)(object)value == (Object)null || !value.IsValid()) { return true; } ItemSettings? obj = ObjectSettings.Read(value.GetZDO()); if (obj == null) { return Store.GetItem(Utils.GetPrefabName(((Component)value).gameObject)).Smoke; } return obj.Smoke; } } [HarmonyPatch(typeof(Fireplace), "CheckWet")] private static class RainPatch { private static readonly FieldRef<Fireplace, bool> Wet = AccessTools.FieldRefAccess<Fireplace, bool>("m_wet"); private static void Postfix(Fireplace __instance) { if (Active && Store.General.IgnoreRain) { Wet.Invoke(__instance) = false; } } } private static readonly Dictionary<int, ZNetView?> ViewBySpawner = new Dictionary<int, ZNetView>(); public static ConfigSettingsStore? Store { get; set; } private static bool Active { get { if (Store != null) { if (!Store.IsMirroring) { return InfernoRuntime.Current != null; } return true; } return false; } } } internal sealed class ClientSync(ConfigSettingsStore store, ManualLogSource log) { private const int MaxHellos = 3; private const float HelloIntervalSeconds = 5f; private const float EditQuietSeconds = 0.75f; private readonly HashSet<string> _editedSections = new HashSet<string>(); private float _sendEditsAt; private ZRoutedRpc? _session; private int _hellos; private float _nextHello; public bool IsSynced => store.IsMirroring; public void Tick(float now) { if (!((Object)(object)ZNet.instance != (Object)null) || ZNet.instance.IsServer() || ZRoutedRpc.instance == null || !((Object)(object)ZNetScene.instance != (Object)null) || !((Object)(object)Player.m_localPlayer != (Object)null)) { if (_session != null) { EndSession(); } return; } if (_session != ZRoutedRpc.instance) { BeginSession(); } if (_editedSections.Count > 0 && now >= _sendEditsAt) { SendEdits(); } if (!IsSynced && _hellos < 3 && now >= _nextHello) { _hellos++; _nextHello = now + 5f; _session.InvokeRoutedRPC("Inferno_Hello", new object[1] { 1 }); } } private void BeginSession() { if (_session != null) { EndSession(); } PrefabDiscovery.Result result = PrefabDiscovery.Run(ZNetScene.instance, log, verbose: false); store.BindItems(result.Catalog); ZRoutedRpc.instance.Register<string>("Inferno_Settings", (Action<long, string>)OnSettings); store.MenuEdited += OnMenuEdited; _session = ZRoutedRpc.instance; _hellos = 0; _nextHello = 0f; } private void EndSession() { store.MenuEdited -= OnMenuEdited; _editedSections.Clear(); store.StopMirroring(); _session = null; } private void OnSettings(long sender, string text) { try { ApplySettings(text); } catch (Exception e) { Diagnostics.Error("applying settings received from the server", e); } } private void ApplySettings(string text) { SettingsMessage val = default(SettingsMessage); if (!SettingsMessage.TryDecode(text, ref val)) { log.LogWarning((object)"Received unreadable settings from the server (different Inferno version?); ignoring."); return; } bool num = !IsSynced; store.ApplyRemote(val); if (num) { log.LogInfo((object)string.Format("Synced Inferno settings from the server ({0} items, {1}).", val.Items.Count, val.CanEdit ? "editable" : "read-only")); } } private void OnMenuEdited(string section) { _editedSections.Add(section); _sendEditsAt = Time.realtimeSinceStartup + 0.75f; } private void SendEdits() { List<string> list = new List<string>(_editedSections); _editedSections.Clear(); if (_session == null) { return; } foreach (string item in list) { foreach (string item2 in store.CommandsFor(item)) { log.LogInfo((object)("Sending to server: " + item2)); _session.InvokeRoutedRPC("Inferno_Command", new object[1] { item2 }); } } } } internal sealed class CommandService(CommandExecutor executor, ConfigSettingsStore store, ManualLogSource log) { private readonly DuplicateFilter _chatCopies = new DuplicateFilter(3.0); private readonly RateLimiter _limiter = new RateLimiter(10, 1.0); private SettingsSnapshot _snapshot = store.Snapshot(); public Func<Vector3, long, float?, NearbySelection>? FindNearby { get; set; } public bool HideCommands => store.General.HideCommands; public event Action? SettingsChanged; public void HandleMenu(long senderUid, string text) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Invalid comparison between Unknown and I4 ZNetPeer val = Players.FindPeer(senderUid); if (val == null) { return; } ParsedCommand command = default(ParsedCommand); string error = default(string); ParseOutcome val2 = CommandParser.Parse(text, ref command, ref error); if ((int)val2 == 0) { log.LogDebug((object)$"Ignoring malformed menu message from {Players.FromPeer(val)}."); return; } bool denied; IReadOnlyList<string> lines = Run("menu", Players.FromPeer(val), text, val2, command, error, null, out denied); Players.ShowMessage(val, lines); if (denied || (int)val2 == 2) { this.SettingsChanged?.Invoke(); } } public bool HandleChat(long senderUid, string text) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) ParsedCommand command = default(ParsedCommand); string error = default(string); ParseOutcome val = CommandParser.Parse(text, ref command, ref error); if ((int)val == 0) { return false; } if (!_chatCopies.IsFirst(senderUid, text.Trim(), (double)Time.realtimeSinceStartup)) { return true; } ZNetPeer val2 = Players.FindPeer(senderUid); if (val2 == null) { log.LogDebug((object)$"Ignoring chat command from unknown peer {senderUid}."); return true; } bool denied; IReadOnlyList<string> lines = Run("chat", Players.FromPeer(val2), text, val, command, error, NearbyOf(val2.m_refPos, val2.m_playerID), out denied); Players.ShowMessage(val2, lines); return true; } public bool HandleConsole(ZRpc rpc, string text) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) ParsedCommand command = default(ParsedCommand); string error = default(string); ParseOutcome val = CommandParser.Parse(text, ref command, ref error); if ((int)val == 0) { return false; } ZNetPeer val2 = Players.FindPeer(rpc); if (val2 == null) { log.LogDebug((object)"Ignoring console command from unknown connection."); return true; } bool denied; IReadOnlyList<string> lines = Run("console", Players.FromPeer(val2), text, val, command, error, NearbyOf(val2.m_refPos, val2.m_playerID), out denied); Players.ConsolePrint(rpc, lines); return true; } public bool HandleSign(ZDO sign, string text, out string answer) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) answer = string.Empty; ParsedCommand command = default(ParsedCommand); string error = default(string); ParseOutcome val = CommandParser.Parse(text, ref command, ref error); if ((int)val == 0) { return false; } ZNetPeer val2 = Players.FindPeer(sign.GetOwner()); CommandSender sender; if (val2 != null) { sender = Players.FromPeer(val2); } else { string text2 = sign.GetString(ZDOVars.s_author, "unknown"); sender = new CommandSender(sign.GetString(ZDOVars.s_authorDisplayName, text2), text2, ZNet.instance.IsAdmin(text2)); } bool denied; IReadOnlyList<string> readOnlyList = Run("sign", sender, text, val, command, error, NearbyOf(sign.GetPosition(), val2?.m_playerID ?? 0), out denied); if (val2 != null) { Players.ShowMessage(val2, readOnlyList); } string text3 = SignReply.Summarize(readOnlyList); answer = ((text3.Length == 0) ? text3 : (RichText.Leading(text) + text3)); return true; } public void OnConfigReloaded() { SettingsSnapshot val = store.Snapshot(); IReadOnlyList<SettingChange> readOnlyList = SettingsDiff.Compare(_snapshot, val); foreach (SettingChange item in readOnlyList) { log.LogInfo((object)$"Config file edited: {item}"); } _snapshot = val; if (readOnlyList.Count > 0) { this.SettingsChanged?.Invoke(); } } private Func<float?, NearbySelection>? NearbyOf(Vector3 spot, long playerId) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) Func<Vector3, long, float?, NearbySelection> find = FindNearby; if (find == null) { return null; } return (float? radius) => find(spot, playerId, radius); } private IReadOnlyList<string> Run(string source, CommandSender sender, string text, ParseOutcome outcome, ParsedCommand command, string error, Func<float?, NearbySelection>? nearby, out bool denied) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 denied = false; RateDecision val = _limiter.TryTake(sender.PlatformId, (double)Time.realtimeSinceStartup); if ((int)val != 1) { if ((int)val == 2) { return Array.Empty<string>(); } log.LogInfo((object)$"{source} command from {sender}: {Shorten(text)}"); if ((int)outcome == 2) { log.LogInfo((object)(" rejected: " + error)); return new <>z__ReadOnlySingleElementList<string>(error); } CommandResult val2; try { val2 = executor.Execute(command, sender, nearby); } catch (Exception e) { Diagnostics.Error("running the " + source + " command '" + Shorten(text) + "'", e); return new <>z__ReadOnlySingleElementList<string>("Something went wrong; see the server log."); } denied = val2.Denied; if (val2.Denied) { log.LogWarning((object)$" denied: {sender} is not an admin and AdminOnly is on."); } foreach (SettingChange change in val2.Changes) { log.LogInfo((object)$" changed by {sender.Name}: {change}"); } if (val2.Changes.Count > 0) { _snapshot = store.Snapshot(); this.SettingsChanged?.Invoke(); } foreach (string item in val2.Reply) { log.LogInfo((object)(" reply: " + item)); } return val2.Reply; } denied = true; log.LogWarning((object)$"{source} commands from {sender} are coming too fast; ignoring them for a few seconds."); return new <>z__ReadOnlySingleElementList<string>("Slow down: too many Inferno commands. Try again in a few seconds."); } private static string Shorten(string text) { string text2 = text.Trim(); if (text2.Length > 200) { return text2.Substring(0, 200) + "…"; } return text2; } } internal sealed class ConfigSettingsStore : ISettingsStore, IDisposable { private sealed class ItemEntries { private ConfigEntry<bool> _alwaysOn; private ConfigEntry<int> _burnRate; private ConfigEntry<bool> _smoke; private ConfigEntry<int>? _onHour; private ConfigEntry<int>? _onMinute; private ConfigEntry<int>? _offHour; private ConfigEntry<int>? _offMinute; public static ItemEntries Bind(ConfigSettingsStore store, CatalogItem item) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) ItemSettings val = ItemSettings.DefaultFor(item.Kind); string prefabName = item.PrefabName; string text = (((int)item.Kind == 0) ? "light source" : "fuel station (no on/off switch, so no schedule)"); ItemEntries itemEntries = new ItemEntries { _alwaysOn = store.Bind(prefabName, "AlwaysOn", val.AlwaysOn, item.DisplayName + " — " + text + ". Keep fuel full at all times. While on, burn rate and schedule are kept but have no effect (setting either by command switches this off)."), _burnRate = store.Bind(prefabName, "BurnRate", val.BurnRateLevel, "Fuel burn rate: 0 = vanilla, each step is 10 %. -10 = uses no fuel, 10 = burns twice as fast.", (AcceptableValueBase?)(object)new AcceptableValueRange<int>(-10, 10)), _smoke = store.Bind(prefabName, "Smoke", val.Smoke, "Whether this item makes smoke. Only players with the Inferno client mod see the smoke removed; others see vanilla smoke.") }; if (item.CanSchedule) { AcceptableValueRange<int> range = new AcceptableValueRange<int>(0, 24); AcceptableValueRange<int> range2 = new AcceptableValueRange<int>(0, 60); itemEntries._onHour = store.Bind(prefabName, "OnTimeHour", 0, "Hour the light turns on. In-game clock (06:00 = sunrise, 18:00 = sunset). On time = off time means no schedule. Only used when AlwaysOn is false.", (AcceptableValueBase?)(object)range); itemEntries._onMinute = store.Bind(prefabName, "OnTimeMinute", 0, "Minute the light turns on.", (AcceptableValueBase?)(object)range2); itemEntries._offHour = store.Bind(prefabName, "OffTimeHour", 0, "Hour the light turns off.", (AcceptableValueBase?)(object)range); itemEntries._offMinute = store.Bind(prefabName, "OffTimeMinute", 0, "Minute the light turns off.", (AcceptableValueBase?)(object)range2); } return itemEntries; } public ItemSettings Read(ManualLogSource log) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown try { DailySchedule val = (DailySchedule)((_onHour == null) ? DailySchedule.AlwaysOn : new DailySchedule(ClockSetting.ToTimeOfDay(_onHour.Value, _onMinute.Value), ClockSetting.ToTimeOfDay(_offHour.Value, _offMinute.Value))); return new ItemSettings(_alwaysOn.Value, _burnRate.Value, val, _smoke.Value); } catch (ArgumentOutOfRangeException ex) { log.LogWarning((object)("Invalid value in config section [" + ((ConfigEntryBase)_alwaysOn).Definition.Section + "]: " + ex.Message + " Using vanilla until fixed.")); return ItemSettings.Vanilla; } } public void Write(ItemSettings settings) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_008f: 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_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) _alwaysOn.Value = settings.AlwaysOn; _burnRate.Value = settings.BurnRateLevel; _smoke.Value = settings.Smoke; if (_onHour != null) { ConfigEntry<int>? onHour = _onHour; DailySchedule schedule = settings.Schedule; TimeOfDay val = ((DailySchedule)(ref schedule)).OnTime; onHour.Value = ((TimeOfDay)(ref val)).Hour; ConfigEntry<int>? onMinute = _onMinute; schedule = settings.Schedule; val = ((DailySchedule)(ref schedule)).OnTime; onMinute.Value = ((TimeOfDay)(ref val)).Minute; ConfigEntry<int>? offHour = _offHour; schedule = settings.Schedule; val = ((DailySchedule)(ref schedule)).OffTime; offHour.Value = ((TimeOfDay)(ref val)).Hour; ConfigEntry<int>? offMinute = _offMinute; schedule = settings.Schedule; val = ((DailySchedule)(ref schedule)).OffTime; offMinute.Value = ((TimeOfDay)(ref val)).Minute; } } } private const string GeneralSection = "General"; private readonly ConfigFile _config; private readonly ManualLogSource _log; private readonly ConfigEntry<bool> _adminOnly; private readonly ConfigEntry<bool> _hideCommands; private readonly ConfigEntry<bool> _ignoreRain; private readonly ConfigEntry<bool> _serverOwnership; private readonly Dictionary<string, ItemEntries> _items = new Dictionary<string, ItemEntries>(StringComparer.OrdinalIgnoreCase); private readonly ConfigurationManagerAttributes _menu = new ConfigurationManagerAttributes(); private FileSystemWatcher? _watcher; private int _fileChanged; private DateTime _ignoreChangesUntil; private SettingsMessage? _remote; private bool _suppressEvents; private bool _saveOnSetBeforeMirror; public string ConfigPath => _config.ConfigFilePath; public bool IsMirroring => _remote != null; public GeneralSettings General { get { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown return new GeneralSettings(_adminOnly.Value, _hideCommands.Value, _ignoreRain.Value, _serverOwnership.Value); } set { Saving(); _adminOnly.Value = value.AdminOnly; _hideCommands.Value = value.HideCommands; _ignoreRain.Value = value.IgnoreRain; _serverOwnership.Value = value.ServerOwnership; } } public event Action<string>? MenuEdited; public ConfigSettingsStore(ConfigFile config, ManualLogSource log) { _config = config; _log = log; _adminOnly = Bind("General", "AdminOnly", GeneralSettings.Default.AdminOnly, "Only admins (the server's adminlist.txt) may change Inferno settings. Anyone may turn this on; only admins may turn it off."); _hideCommands = Bind("General", "HideCommands", GeneralSettings.Default.HideCommands, "Hide '!fires' chat commands from other players."); _ignoreRain = Bind("General", "IgnoreRain", GeneralSettings.Default.IgnoreRain, "PC client mod only: players with the client mod see fires burn normally in rain. Everyone else sees vanilla rain. Side effect: the server also switches lights back on that a player switched off by hand."); _serverOwnership = Bind("General", "ServerOwnership", GeneralSettings.Default.ServerOwnership, "EXPERIMENTAL. The server keeps ownership of fires, so no player's game burns their fuel or switches them off in rain. Burn rate and schedule become exact. Side effects: no sound when adding fuel or switching these fires; they don't go out in rain. Fires that melt snow or spread fire are excluded. Turning it off gives the fires back immediately."); _config.SettingChanged += OnSettingChanged; } public void BindItems(ItemCatalog catalog) { bool saveOnConfigSet = _config.SaveOnConfigSet; _config.SaveOnConfigSet = false; try { foreach (CatalogItem item in catalog.Items) { if (!_items.ContainsKey(item.PrefabName)) { _items.Add(item.PrefabName, ItemEntries.Bind(this, item)); } } } finally { _config.SaveOnConfigSet = saveOnConfigSet; } if (!IsMirroring) { Saving(); _config.Save(); } } public ItemSettings GetItem(string prefabName) { if (!_items.TryGetValue(prefabName, out ItemEntries value)) { return ItemSettings.Vanilla; } return value.Read(_log); } public void SetItem(string prefabName, ItemSettings settings) { if (_items.TryGetValue(prefabName, out ItemEntries value)) { Saving(); value.Write(settings); } } public ItemSettings? GetObject(string objectKey) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (ObjectSettings.TryParseKey(objectKey, out var id)) { ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(id) : null); if (val != null) { return ObjectSettings.Read(val); } } return null; } public void SetObject(string objectKey, ItemSettings? settings) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (ObjectSettings.TryParseKey(objectKey, out var id)) { ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(id) : null); if (val != null) { ObjectSettings.Write(val, settings); } } } public SettingsSnapshot Snapshot() { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown Dictionary<string, ItemSettings> dictionary = new Dictionary<string, ItemSettings>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, ItemEntries> item in _items) { dictionary[item.Key] = item.Value.Read(_log); } return new SettingsSnapshot(General, (IReadOnlyDictionary<string, ItemSettings>)dictionary); } public void ApplyRemote(SettingsMessage message) { if (_remote == null) { _saveOnSetBeforeMirror = _config.SaveOnConfigSet; _config.SaveOnConfigSet = false; } _suppressEvents = true; try { General = message.General; foreach (KeyValuePair<string, ItemSettings> item in message.Items) { if (_items.TryGetValue(item.Key, out ItemEntries value)) { value.Write(item.Value); } } _menu.ReadOnly = !message.CanEdit; _remote = message; } finally { _suppressEvents = false; } } public void StopMirroring() { if (_remote == null) { return; } _suppressEvents = true; try { _config.Reload(); _config.SaveOnConfigSet = _saveOnSetBeforeMirror; _menu.ReadOnly = false; _remote = null; } finally { _suppressEvents = false; } } public void WatchFile() { string configFilePath = _config.ConfigFilePath; _watcher?.Dispose(); _watcher = new FileSystemWatcher(Path.GetDirectoryName(configFilePath), Path.GetFileName(configFilePath)) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite) }; _watcher.Changed += delegate { Interlocked.Exchange(ref _fileChanged, 1); }; _watcher.Created += delegate { Interlocked.Exchange(ref _fileChanged, 1); }; _watcher.Renamed += delegate { Interlocked.Exchange(ref _fileChanged, 1); }; _watcher.EnableRaisingEvents = true; } public bool ReloadIfChanged() { if (Interlocked.Exchange(ref _fileChanged, 0) == 0 || DateTime.UtcNow < _ignoreChangesUntil) { return false; } try { _config.Reload(); return true; } catch (IOException ex) { _log.LogWarning((object)("Could not reload config yet (" + ex.Message + "); will retry.")); Interlocked.Exchange(ref _fileChanged, 1); return false; } } public void Dispose() { _watcher?.Dispose(); _watcher = null; } private ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, string description, AcceptableValueBase? range = null) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown return _config.Bind<T>(section, key, defaultValue, new ConfigDescription(description, range, new object[1] { _menu })); } public IReadOnlyList<string> CommandsFor(string section) { SettingsMessage remote = _remote; if (remote == null) { return Array.Empty<string>(); } if (section == "General") { return MenuCommands.ForGeneral(remote.General, General); } if (!remote.Items.TryGetValue(section, out var value)) { return Array.Empty<string>(); } return MenuCommands.ForItem(section, value, GetItem(section)); } private void OnSettingChanged(object sender, SettingChangedEventArgs e) { if (!_suppressEvents && _remote != null) { this.MenuEdited?.Invoke(e.ChangedSetting.Definition.Section); } } private void Saving() { _ignoreChangesUntil = DateTime.UtcNow.AddSeconds(2.0); } } internal sealed class ConfigurationManagerAttributes { public bool? ReadOnly; } internal static class Diagnostics { private const double RepeatSummarySeconds = 300.0; private static readonly ErrorThrottle Throttle = new ErrorThrottle(300.0); private static ManualLogSource? _log; public static int ErrorCount => Throttle.Total; public static void Init(ManualLogSource log) { _log = log; } public static void Error(string where, Exception e) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) ErrorLogDecision val = Throttle.Record(where + ":" + e.GetType().FullName, (double)Time.realtimeSinceStartup); if (((ErrorLogDecision)(ref val)).Log && _log != null) { if (((ErrorLogDecision)(ref val)).FirstTime) { _log.LogError((object)("Error while " + where + ". Inferno skipped this and keeps running. Please report it with this log " + string.Format("(Inferno {0}, Valheim {1}):\n{2}", "0.3.2", Version.CurrentVersion, e))); } else { _log.LogError((object)($"Error while {where} happened again ({((ErrorLogDecision)(ref val)).Suppressed} more time(s) in the last " + $"{5.0:0} min): {e.GetType().Name}: {e.Message}")); } } } public static void LogStartup(Harmony? harmony, string configPath) { if (_log == null) { return; } try { WriteStartup(harmony, configPath); } catch (Exception e) { Error("writing the startup diagnostics", e); } } private static void WriteStartup(Harmony? harmony, string configPath) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Invalid comparison between Unknown and I4 //IL_00b7: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("--- Inferno startup diagnostics (include this when reporting a problem) ---"); stringBuilder.AppendLine(string.Format("Inferno {0} | Valheim {1} (network {2}) | BepInEx {3}", "0.3.2", Version.CurrentVersion, 40u, typeof(Chainloader).Assembly.GetName().Version)); stringBuilder.AppendLine(string.Format("Server: {0} | crossplay {1} | world '{2}' | OS {3} ({4})", instance.IsDedicated() ? "dedicated" : "hosted from game", ((int)ZNet.m_onlineBackend == 1) ? "on" : "off", instance.GetWorldName(), Environment.OSVersion, Application.platform)); stringBuilder.AppendLine("Config: " + configPath); List<string> list = (from p in Chainloader.PluginInfos.Values where p.Metadata.GUID != "GrundleLord.Inferno" select $"{p.Metadata.Name} {p.Metadata.Version}").OrderBy<string, string>((string n) => n, StringComparer.OrdinalIgnoreCase).ToList(); stringBuilder.AppendLine((list.Count == 0) ? "Other mods: none" : string.Format("Other mods ({0}): {1}", list.Count, string.Join(", ", list))); stringBuilder.Append(SharedHooks(harmony)); _log.LogInfo((object)stringBuilder.ToString().TrimEnd(Array.Empty<char>())); } private static string SharedHooks(Harmony? harmony) { if (harmony == null) { return "Shared hooks: unknown"; } StringBuilder stringBuilder = new StringBuilder(); foreach (MethodBase patchedMethod in harmony.GetPatchedMethods()) { Patches patchInfo = Harmony.GetPatchInfo(patchedMethod); List<string> list = ((patchInfo != null) ? patchInfo.Owners.Where((string o) => o != harmony.Id).ToList() : null); if (list != null && list.Count > 0) { stringBuilder.Append(patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name + " (also patched by " + string.Join(", ", list) + "); "); } } if (stringBuilder.Length != 0) { return "Shared hooks: " + stringBuilder.ToString().TrimEnd(' ', ';'); } return "Shared hooks: none (no other mod patches the game methods Inferno uses)"; } } internal sealed class FuelScanner(PrefabDiscovery.Result discovery, ISettingsStore store, CommandService commands, ServerOwnership ownership, ManualLogSource log) { private const float ApplyIntervalSeconds = 5f; private const float SoonSeconds = 0.5f; private const float SweepIntervalSeconds = 30f; private const int SweepChunkSize = 20000; private const long FallbackDayLengthSeconds = 1800L; private const uint RevisionLead = 8u; private static readonly int SwitchedOffByInfernoKey = StringExtensionMethods.GetStableHashCode("Inferno_ScheduledOff"); private static readonly FieldRef<ZDOMan, Dictionary<ZDOID, ZDO>> ObjectsById = AccessTools.FieldRefAccess<ZDOMan, Dictionary<ZDOID, ZDO>>("m_objectsByID"); private readonly List<ZDO> _sweepBuffer = new List<ZDO>(); private readonly List<ZDOID> _sweepFuel = new List<ZDOID>(); private readonly List<ZDOID> _sweepSigns = new List<ZDOID>(); private readonly Dictionary<ZDOID, float> _baselines = new Dictionary<ZDOID, float>(); private readonly Dictionary<string, ItemSettings> _settingsCache = new Dictionary<string, ItemSettings>(); private readonly HashSet<ZDOID> _fuelSet = new HashSet<ZDOID>(); private readonly HashSet<ZDOID> _signSet = new HashSet<ZDOID>(); private readonly HashSet<ZDOID> _wardSet = new HashSet<ZDOID>(); private readonly List<ZDOID> _sweepWards = new List<ZDOID>(); private readonly List<ZDOID> _arrivedSinceSweep = new List<ZDOID>(); private const float StatsIntervalSeconds = 600f; private readonly RunningStats _passStats = new RunningStats(); private readonly RunningStats _sweepStats = new RunningStats(); private readonly Stopwatch _sweepWatch = new Stopwatch(); private int _writesSinceStats; private float _nextStats = 600f; private List<ZDOID> _fuel = new List<ZDOID>(); private List<ZDOID> _signs = new List<ZDOID>(); private int _sweepIndex = -1; private float _nextSweep; private float _nextApply; private bool _warnedDayLength; public string StatusText { get { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) int num = 0; int num2 = 0; int num3 = 0; foreach (ZDOID item in _fuel) { ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(item) : null); if (val != null) { if (ObjectSettings.Read(val) != null) { num3++; } if (ServerOwnership.IsServerSimulated(val)) { num++; } else if (!val.HasOwner()) { num2++; } } } return $"Tracking {_fuel.Count} fuel object(s): {num2} with nobody nearby, {num} handled by the server, {num3} with own settings; {_signs.Count} sign(s), {_wardSet.Count} ward(s)."; } } public string CurrentTimeText => ((object)CurrentTime()/*cast due to .constrained prefix*/).ToString(); public void Notice(ZDO zdo) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) int prefab = zdo.GetPrefab(); if (discovery.ByHash.ContainsKey(prefab)) { if (_fuelSet.Add(zdo.m_uid)) { _fuel.Add(zdo.m_uid); _arrivedSinceSweep.Add(zdo.m_uid); ApplySoon(); } } else if (discovery.SignHashes.Contains(prefab)) { if (_signSet.Add(zdo.m_uid)) { _signs.Add(zdo.m_uid); _arrivedSinceSweep.Add(zdo.m_uid); } ApplySoon(); } else if (discovery.WardRadii.ContainsKey(prefab)) { _wardSet.Add(zdo.m_uid); } } public List<Ward> Wards() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Expected O, but got Unknown List<Ward> list = new List<Ward>(_wardSet.Count); foreach (ZDOID item in _wardSet) { ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(item) : null); if (val != null && discovery.WardRadii.TryGetValue(val.GetPrefab(), out var value)) { List<long> list2 = new List<long>(); int num = val.GetInt(ZDOVars.s_permitted, 0); for (int i = 0; i < num; i++) { list2.Add(val.GetLong("pu_id" + i.ToString(CultureInfo.InvariantCulture), 0L)); } Vector3 position = val.GetPosition(); list.Add(new Ward(position.x, position.z, value, val.GetBool(ZDOVars.s_enabled, false), val.GetLong(ZDOVars.s_creator, 0L), (IReadOnlyCollection<long>)list2)); } } return list; } public NearbySelection Nearby(Vector3 spot, long playerId, float? radius) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: 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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Expected O, but got Unknown //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Expected O, but got Unknown List<Ward> list = Wards(); NearbyArea obj; if (radius.HasValue) { float valueOrDefault = radius.GetValueOrDefault(); obj = NearbyArea.Circle(spot.x, spot.z, valueOrDefault); } else { obj = NearbyArea.Around((IEnumerable<Ward>)list, spot.x, spot.z, 20f); } NearbyArea val = obj; List<NearbyObject> list2 = new List<NearbyObject>(); foreach (ZDOID item in _fuel) { ZDOMan instance = ZDOMan.instance; ZDO val2 = ((instance != null) ? instance.GetZDO(item) : null); if (val2 != null && discovery.ByHash.TryGetValue(val2.GetPrefab(), out CatalogItem value)) { Vector3 position = val2.GetPosition(); if (val.Contains(position.x, position.z)) { list2.Add(new NearbyObject(ObjectSettings.KeyOf(item), value.PrefabName, NearbyArea.CanAccess((IEnumerable<Ward>)list, position.x, position.z, playerId))); } } } return new NearbySelection((IReadOnlyList<NearbyObject>)list2, val.Description); } public void Tick(float now) { if (ZDOMan.instance != null && ZNet.instance != null) { if (_sweepIndex < 0 && now >= _nextSweep) { BeginSweep(); } if (_sweepIndex >= 0) { ContinueSweep(now); } ownership.Tick(now); if (now >= _nextApply) { _nextApply = now + 5f; Stopwatch stopwatch = Stopwatch.StartNew(); ApplySettings(now); HandleSigns(); _passStats.Add(stopwatch.Elapsed.TotalMilliseconds); } if (now >= _nextStats) { _nextStats = now + 600f; log.LogInfo((object)$"Performance (last {10f:0} min): {StatusText} Passes: {_passStats}. Sweeps: {_sweepStats}. Writes: {_writesSinceStats}. Errors since start: {Diagnostics.ErrorCount}."); _passStats.Reset(); _sweepStats.Reset(); _writesSinceStats = 0; } } } public void ReleaseOwnership() { if (ZDOMan.instance != null) { int num = ownership.ReleaseAll(ObjectsById.Invoke(ZDOMan.instance).Values); if (num > 0) { log.LogInfo((object)$"Gave {num} server-owned fire(s) back to the game."); } } } public void RestoreForShutdown() { //IL_0057: 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_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) if (ZDOMan.instance == null) { return; } int num = 0; foreach (ZDO value2 in ObjectsById.Invoke(ZDOMan.instance).Values) { if (value2 == null || !discovery.ByHash.TryGetValue(value2.GetPrefab(), out CatalogItem value) || !value.CanSchedule) { continue; } try { FuelDecision decision = FuelController.RestoreForShutdown(ReadState(value2, value)); if (((FuelDecision)(ref decision)).HasChanges) { Apply(value2, decision); num++; } } catch (Exception e) { Diagnostics.Error("switching " + value.PrefabName + " back on at shutdown", e); } } log.LogInfo((object)$"Shutdown: switched {num} scheduled-off light(s) back on before saving."); } private void BeginSweep() { _sweepBuffer.Clear(); _sweepBuffer.AddRange(ObjectsById.Invoke(ZDOMan.instance).Values); _sweepFuel.Clear(); _sweepSigns.Clear(); _sweepWards.Clear(); _arrivedSinceSweep.Clear(); _sweepIndex = 0; } private void ContinueSweep(float now) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_0214: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02d0: Unknown result type (might be due to invalid IL or missing references) //IL_02d5: Unknown result type (might be due to invalid IL or missing references) //IL_02dd: Unknown result type (might be due to invalid IL or missing references) _sweepWatch.Start(); int num = Math.Min(_sweepIndex + 20000, _sweepBuffer.Count); while (_sweepIndex < num) { try { ZDO val = _sweepBuffer[_sweepIndex]; if (val != null && val.IsValid()) { int prefab = val.GetPrefab(); if (discovery.ByHash.ContainsKey(prefab)) { _sweepFuel.Add(val.m_uid); } else if (discovery.SignHashes.Contains(prefab)) { _sweepSigns.Add(val.m_uid); } else if (discovery.WardRadii.ContainsKey(prefab)) { _sweepWards.Add(val.m_uid); } } } catch (Exception e) { Diagnostics.Error("looking for fires and signs in the world", e); } _sweepIndex++; } _sweepWatch.Stop(); if (_sweepIndex < _sweepBuffer.Count) { return; } _sweepStats.Add(_sweepWatch.Elapsed.TotalMilliseconds); _sweepWatch.Reset(); _fuelSet.Clear(); _signSet.Clear(); _fuelSet.UnionWith(_sweepFuel); _signSet.UnionWith(_sweepSigns); _wardSet.RemoveWhere(delegate(ZDOID id) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) ZDOMan instance2 = ZDOMan.instance; return ((instance2 != null) ? instance2.GetZDO(id) : null) == null; }); _wardSet.UnionWith(_sweepWards); foreach (ZDOID item in _arrivedSinceSweep) { ZDOMan instance = ZDOMan.instance; ZDO val2 = ((instance != null) ? instance.GetZDO(item) : null); if (val2 != null) { if (discovery.ByHash.ContainsKey(val2.GetPrefab())) { _fuelSet.Add(item); } else { _signSet.Add(item); } } } List<ZDOID> fuel = new List<ZDOID>(_fuelSet); List<ZDOID> signs = new List<ZDOID>(_signSet); _fuel = fuel; _signs = signs; _arrivedSinceSweep.Clear(); HashSet<ZDOID> hashSet = new HashSet<ZDOID>(_fuel); List<ZDOID> list = new List<ZDOID>(); foreach (ZDOID key in _baselines.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (ZDOID item2 in list) { _baselines.Remove(item2); } _sweepBuffer.Clear(); _sweepIndex = -1; _nextSweep = now + 30f; log.LogDebug((object)$"Sweep done: tracking {_fuel.Count} fuel object(s) and {_signs.Count} sign(s)."); } private void ApplySettings(float realNow) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) TimeOfDay val = CurrentTime(); bool ignoreRain = store.General.IgnoreRain; int num = 0; _settingsCache.Clear(); foreach (ZDOID item in _fuel) { ZDO zDO = ZDOMan.instance.GetZDO(item); if (zDO == null || !discovery.ByHash.TryGetValue(zDO.GetPrefab(), out CatalogItem value)) { continue; } try { ownership.Manage(zDO, realNow); float value2; float? num2 = (_baselines.TryGetValue(item, out value2) ? new float?(value2) : ((float?)null)); ItemSettings value3 = ObjectSettings.Read(zDO); if (value3 == null && !_settingsCache.TryGetValue(value.PrefabName, out value3)) { value3 = store.GetItem(value.PrefabName); _settingsCache[value.PrefabName] = value3; } FuelDecision decision = FuelController.Decide(value3, value.Fuel, ReadState(zDO, value), num2, val, ignoreRain); _baselines[item] = ((FuelDecision)(ref decision)).Baseline; if (((FuelDecision)(ref decision)).HasChanges) { Apply(zDO, decision); num++; _writesSinceStats++; } } catch (Exception e) { Diagnostics.Error("applying settings to " + value.PrefabName, e); } } if (num > 0) { log.LogDebug((object)$"Applied settings to {num} of {_fuel.Count} object(s) at {val}."); } } private void HandleSigns() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) foreach (ZDOID sign in _signs) { ZDO zDO = ZDOMan.instance.GetZDO(sign); string text = ((zDO != null) ? zDO.GetString(ZDOVars.s_text, "") : null); if (zDO == null || string.IsNullOrEmpty(text)) { continue; } string answer; try { if (!commands.HandleSign(zDO, text, out answer)) { continue; } } catch (Exception e) { Diagnostics.Error("running a sign command", e); answer = "Inferno error; see server log."; } zDO.Set(ZDOVars.s_text, answer); } } private void ApplySoon() { _nextApply = Math.Min(_nextApply, Time.realtimeSinceStartup + 0.5f); } private static FuelState ReadState(ZDO zdo, CatalogItem item) { //IL_0097: Unknown result type (might be due to invalid IL or missing references) long num = zdo.GetLong(ZDOVars.s_lastTime, 0L); double num2 = ((num > 0) ? Math.Max(0.0, (double)(ZNet.instance.GetTime().Ticks - num) / 10000000.0) : 0.0); return new FuelState(zdo.GetFloat(ZDOVars.s_fuel, 0f), !item.CanSchedule || zdo.GetInt(ZDOVars.s_state, 1) == 1, zdo.GetInt(SwitchedOffByInfernoKey, 0) == 1, !zdo.HasOwner() || ServerOwnership.IsServerSimulated(zdo), num2); } private static void Apply(ZDO zdo, FuelDecision decision) { float? setFuel = ((FuelDecision)(ref decision)).SetFuel; if (setFuel.HasValue) { float valueOrDefault = setFuel.GetValueOrDefault(); zdo.Set(ZDOVars.s_fuel, valueOrDefault); } bool? setSwitchedOn = ((FuelDecision)(ref decision)).SetSwitchedOn; if (setSwitchedOn.HasValue) { bool valueOrDefault2 = setSwitchedOn == true; zdo.Set(ZDOVars.s_state, valueOrDefault2 ? 1 : 2, false); } setSwitchedOn = ((FuelDecision)(ref decision)).SetSwitchedOffByInferno; if (setSwitchedOn.HasValue) { bool valueOrDefault3 = setSwitchedOn == true; zdo.Set(SwitchedOffByInfernoKey, valueOrDefault3 ? 1 : 0, false); } if (((FuelDecision)(ref decision)).RefreshBurnClock) { zdo.Set(ZDOVars.s_lastTime, ZNet.instance.GetTime().Ticks); } if (((FuelDecision)(ref decision)).MustWin) { zdo.DataRevision += 8; } } private TimeOfDay CurrentTime() { //IL_0070: Unknown result type (might be due to invalid IL or missing references) long num = (((Object)(object)EnvMan.instance != (Object)null) ? EnvMan.instance.m_dayLengthSec : 0); if (num <= 0) { if (!_warnedDayLength) { log.LogWarning((object)$"Day length not available on this server; assuming {1800L} s for schedules."); _warnedDayLength = true; } num = 1800L; } return GameClock.TimeAt(Math.Max(0.0, ZNet.instance.GetTimeSeconds()), num); } } internal static class GameHooks { [HarmonyPatch(typeof(ZRoutedRpc), "RouteRPC")] private static class ChatRelayPatch { private static bool Prefix(RoutedRPCData rpcData) { InfernoRuntime current = InfernoRuntime.Current; if (current == null || (rpcData.m_methodHash != SayHash && rpcData.m_methodHash != ChatMessageHash)) { return true; } try { string text = ReadChatText(rpcData); if (text == null || !current.Commands.HandleChat(rpcData.m_senderPeerID, text)) { return true; } return !current.Commands.HideCommands; } catch (Exception e) { Diagnostics.Error("checking a chat message for commands (message passed through unchanged)", e); return true; } } private static string? ReadChatText(RoutedRPCData rpcData) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) ZPackage val = new ZPackage(rpcData.m_parameters.GetArray()); if (rpcData.m_methodHash == ChatMessageHash) { val.ReadVector3(); } val.ReadInt(); val.ReadString(); val.ReadString(); return val.ReadString(); } } [HarmonyPatch(typeof(ZNet), "RPC_RemoteCommand")] private static class RemoteCommandPatch { private static bool Prefix(ZRpc rpc, string command) { InfernoRuntime current = InfernoRuntime.Current; if (current == null) { return true; } try { return !current.Commands.HandleConsole(rpc, command); } catch (Exception e) { Diagnostics.Error("handling an F5 console command (passed on to the game)", e); return true; } } } [HarmonyPatch(typeof(ZRoutedRpc), "HandleRoutedRPC")] private static class OwnedFireRpcPatch { private static bool Prefix(RoutedRPCData data) { InfernoRuntime current = InfernoRuntime.Current; if (current == null) { return true; } try { return !current.Ownership.TryHandleRpc(data); } catch (Exception e) { Diagnostics.Error("handling a player action on a server-owned fire (passed on to the game)", e); return true; } } } [HarmonyPatch(typeof(ZDOMan), "ReleaseNearbyZDOS")] private static class KeepOwnershipPatch { [ThreadStatic] private static bool _active; public static bool Active => _active; private static void Prefix() { _active = true; } private static Exception? Finalizer(Exception? __exception) { _active = false; return __exception; } } [HarmonyPatch(typeof(ZDO), "SetOwner")] private static class SetOwnerPatch { private static bool Prefix(ZDO __instance) { if (!KeepOwnershipPatch.Active) { return true; } InfernoRuntime current = InfernoRuntime.Current; try { return current == null || !current.Ownership.IsProtected(__instance); } catch (Exception e) { Diagnostics.Error("keeping a fire server-owned (vanilla owner change allowed)", e); return true; } } } [HarmonyPatch(typeof(ZDO), "Deserialize")] private static class ObjectArrivedPatch { private static void Postfix(ZDO __instance) { InfernoRuntime current = InfernoRuntime.Current; if (current == null) { return; } try { current.Scanner.Notice(__instance); } catch (Exception e) { Diagnostics.Error("noticing a new or changed object", e); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] private static class ShutdownPatch { private static void Prefix() { InfernoRuntime current = InfernoRuntime.Current; if (current == null) { return; } try { current.Scanner.RestoreForShutdown(); current.Scanner.ReleaseOwnership(); } catch (Exception e) { Diagnostics.Error("restoring lights at shutdown", e); } } } private static readonly int SayHash = StringExtensionMethods.GetStableHashCode("Say"); private static readonly int ChatMessageHash = StringExtensionMethods.GetStableHashCode("ChatMessage"); } internal sealed class InfernoRuntime : IDisposable { private readonly SyncServer _sync; private readonly PrefabDiscovery.Result _discovery; public static InfernoRuntime? Current { get; private set; } public ManualLogSource Log { get; } public ConfigSettingsStore Store { get; } public CommandService Commands { get; } public FuelScanner Scanner { get; } public ServerOwnership Ownership { get; } private InfernoRuntime(ManualLogSource log, ConfigSettingsStore store, CommandService commands, FuelScanner scanner, ServerOwnership ownership, SyncServer sync, PrefabDiscovery.Result discovery) { _sync = sync; _discovery = discovery; Log = log; Store = store; Commands = commands; Scanner = scanner; Ownership = ownership; } public static void Start(ConfigSettingsStore store, ManualLogSource log, Harmony? harmony) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown Diagnostics.LogStartup(harmony, store.ConfigPath); PrefabDiscovery.Result result = PrefabDiscovery.Run(ZNetScene.instance, log); store.BindItems(result.Catalog); store.WatchFile(); CommandService commandService = new CommandService(new CommandExecutor(result.Catalog, (ISettingsStore)(object)store, (Func<IReadOnlyList<string>>)(() => Current?.StatusLines() ?? Array.Empty<string>())), store, log); ServerOwnership ownership = new ServerOwnership(result, (ISettingsStore)(object)store, log); FuelScanner fuelScanner = new FuelScanner(result, (ISettingsStore)(object)store, commandService, ownership, log); commandService.FindNearby = fuelScanner.Nearby; SyncServer syncServer = new SyncServer(store, commandService, log); syncServer.Register(ZRoutedRpc.instance); Current = new InfernoRuntime(log, store, commandService, fuelScanner, ownership, syncServer, result); GeneralSettings general = store.General; log.LogInfo((object)$"Running on server. AdminOnly={general.AdminOnly}, HideCommands={general.HideCommands}, IgnoreRain={general.IgnoreRain}, ServerOwnership={general.ServerOwnership}."); } public static void Stop() { if (Current == null) { return; } try { Current.Scanner.ReleaseOwnership(); } finally { Current.Dispose(); Current = null; } } public void Tick(float now) { if (Store.ReloadIfChanged()) { Commands.OnConfigReloaded(); } Scanner.Tick(now); _sync.Tick(now); } public IReadOnlyList<string> StatusLines() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) GeneralSettings general = Store.General; int num = 0; foreach (CatalogItem item in _discovery.Catalog.Items) { if ((int)item.Kind == 0) { num++; } } string text = ((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString(); return new <>z__ReadOnlyArray<string>(new string[4] { "Inferno 0.3.2 running on Valheim " + text + ".", $"Item types: {num} light(s), {_discovery.Catalog.Items.Count - num} station(s). {Scanner.StatusText}", "In-game time " + Scanner.CurrentTimeText + ". AdminOnly " + OnOff(general.AdminOnly) + ", ServerOwnership " + OnOff(general.ServerOwnership) + ".", (Diagnostics.ErrorCount == 0) ? "Errors since start: 0." : $"Errors since start: {Diagnostics.ErrorCount} (see the server log)." }); } private static string OnOff(bool value) { if (!value) { return "off"; } return "on"; } public void Dispose() { _sync.Unregister(); Store.Dispose(); } } internal static class ObjectSettings { private static readonly int OwnKey = StringExtensionMethods.GetStableHashCode("Inferno_Own"); private static readonly int AlwaysOnKey = StringExtensionMethods.GetStableHashCode("Inferno_AlwaysOn"); private static readonly int BurnRateKey = StringExtensionMethods.GetStableHashCode("Inferno_BurnRate"); private static readonly int OnMinutesKey = StringExtensionMethods.GetStableHashCode("Inferno_OnMinutes"); private static readonly int OffMinutesKey = StringExtensionMethods.GetStableHashCode("Inferno_OffMinutes"); private static readonly int SmokeKey = StringExtensionMethods.GetStableHashCode("Inferno_Smoke"); private const uint RevisionLead = 8u; public static string KeyOf(ZDOID id) { return ((ZDOID)(ref id)).UserID.ToString(CultureInfo.InvariantCulture) + ":" + ((ZDOID)(ref id)).ID.ToString(CultureInfo.InvariantCulture); } public static bool TryParseKey(string key, out ZDOID id) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) id = ZDOID.None; string[] array = key.Split(new char[1] { ':' }); if (array.Length != 2 || !long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !uint.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { return false; } id = new ZDOID(result, result2); return true; } public static ItemSettings? Read(ZDO zdo) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown if (zdo.GetInt(OwnKey, 0) != 1) { return null; } int num = zdo.GetInt(BurnRateKey, 0); if ((num < -10 || num > 10) ? true : false) { num = 0; } return new ItemSettings(zdo.GetInt(AlwaysOnKey, 0) == 1, num, new DailySchedule(Minutes(zdo.GetInt(OnMinutesKey, 0)), Minutes(zdo.GetInt(OffMinutesKey, 0))), zdo.GetInt(SmokeKey, 1) == 1); } public static void Write(ZDO zdo, ItemSettings? settings) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if (settings == null) { zdo.Set(OwnKey, 0, false); } else { zdo.Set(AlwaysOnKey, settings.AlwaysOn ? 1 : 0, false); zdo.Set(BurnRateKey, settings.BurnRateLevel, false); int onMinutesKey = OnMinutesKey; DailySchedule schedule = settings.Schedule; TimeOfDay val = ((DailySchedule)(ref schedule)).OnTime; zdo.Set(onMinutesKey, ((TimeOfDay)(ref val)).MinutesSinceMidnight, false); int offMinutesKey = OffMinutesKey; schedule = settings.Schedule; val = ((DailySchedule)(ref schedule)).OffTime; zdo.Set(offMinutesKey, ((TimeOfDay)(ref val)).MinutesSinceMidnight, false); zdo.Set(SmokeKey, settings.Smoke ? 1 : 0, false); zdo.Set(OwnKey, 1, false); } zdo.DataRevision += 8; } private static TimeOfDay Minutes(int minutes) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (minutes < 0 || minutes >= 1440) { return default(TimeOfDay); } return TimeOfDay.FromHourMinute(minutes / 60, minutes % 60); } } internal static class Players { private const int MaxOnScreenLines = 6; public static ZNetPeer? FindPeer(long uid) { if (uid != 0L) { ZNet instance = ZNet.instance; if (instance == null) { return null; } return instance.GetPeer(uid); } return null; } public static ZNetPeer? FindPeer(ZRpc rpc) { ZNet instance = ZNet.instance; if (instance == null) { return null; } foreach (ZNetPeer peer in instance.GetPeers()) { if (peer.m_rpc == rpc) { return peer; } } return null; } public static CommandSender FromPeer(ZNetPeer peer) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown ISocket socket = peer.m_socket; string text = ((socket != null) ? socket.GetHostName() : null) ?? "unknown"; return new CommandSender(string.IsNullOrEmpty(peer.m_playerName) ? text : peer.m_playerName, text, ZNet.instance.IsAdmin(text)); } public static void ShowMessage(ZNetPeer peer, IReadOnlyList<string> lines) { if (lines.Count != 0) { string text = "[Inferno] " + string.Join("\n", ReplyLimiter.Limit(lines, 6)); ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "ShowMessage", new object[2] { 1, text }); } } public static void ConsolePrint(ZRpc rpc, IReadOnlyList<string> lines) { foreach (string line in lines) { ZNet.instance.RemotePrint(rpc, "[Inferno] " + line); } } } internal static class PrefabDiscovery { [Flags] public enum FireExtras { None = 0, MeltsSnow = 1, SpreadsFire = 2 } public sealed class Result { public Dictionary<int, FireExtras> OwnershipCandidates { get; } public ItemCatalog Catalog { get; } public Dictionary<int, CatalogItem> ByHash { get; } public HashSet<int> SignHashes { get; } public Dictionary<int, float> WardRadii { get; } public Result(ItemCatalog catalog, Dictionary<int, CatalogItem> byHash, HashSet<int> signHashes, Dictionary<int, FireExtras> ownershipCandidates) { OwnershipCandidates = ownershipCandidates; Catalog = catalog; ByHash = byHash; SignHashes = signHashes; WardRadii = new Dictionary<int, float>(); base..ctor(); } } public static Result Run(ZNetScene scene, ManualLogSource log, bool verbose = true) { //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Expected O, but got Unknown //IL_020e: Unknown result type (might be due to invalid IL or missing references) List<CatalogItem> list = new List<CatalogItem>(); Dictionary<int, CatalogItem> dictionary = new Dictionary<int, CatalogItem>(); HashSet<int> hashSet = new HashSet<int>(); List<string> list2 = new List<string>(); Dictionary<int, FireExtras> dictionary2 = new Dictionary<int, FireExtras>(); List<string> list3 = new List<string>(); List<string> list4 = new List<string>(); foreach (GameObject prefab in scene.m_prefabs) { if ((Object)(object)prefab == (Object)null) { continue; } if ((Object)(object)prefab.GetComponent<Sign>() != (Object)null) { hashSet.Add(StringExtensionMethods.GetStableHashCode(((Object)prefab).name)); } if (IsRainSensitive(prefab)) { list2.Add(((Object)prefab).name); } CatalogItem val = TryCreateItem(prefab, log); if (val == null || dictionary.ContainsKey(StringExtensionMethods.GetStableHashCode(((Object)prefab).name))) { continue; } list.Add(val); dictionary.Add(StringExtensionMethods.GetStableHashCode(((Object)prefab).name), val); if (!val.Fuel.BurnsOverTime) { continue; } if (HasOtherOwnerParts(prefab)) { list3.Add(((Object)prefab).name); continue; } FireExtras fireExtras = ExtrasOf(prefab.GetComponent<Fireplace>()); dictionary2.Add(StringExtensionMethods.GetStableHashCode(((Object)prefab).name), fireExtras); if (fireExtras != FireExtras.None) { list4.Add($"{((Object)prefab).name} ({fireExtras})"); } } ItemCatalog val2 = new ItemCatalog((IEnumerable<CatalogItem>)list); Result result = new Result(val2, dictionary, hashSet, dictionary2); foreach (GameObject prefab2 in scene.m_prefabs) { PrivateArea val3 = (((Object)(object)prefab2 == (Object)null) ? null : prefab2.GetComponent<PrivateArea>()); if ((Object)(object)val3 != (Object)null) { result.WardRadii[StringExtensionMethods.GetStableHashCode(((Object)prefab2).name)] = val3.m_radius; } } if (!verbose) { return result; } foreach (CatalogItem item in val2.Items) { log.LogInfo((object)string.Format("Found {0}: {1} ({2}), max fuel {3}", ((int)item.Kind == 0) ? "light source" : "fuel station", item.PrefabName, item.DisplayName, item.Fuel.MaxFuel)); } log.LogInfo((object)$"Discovered {val2.Items.Count} fuel-burning item types and {hashSet.Count} sign types."); log.LogInfo((object)((list2.Count == 0) ? "No lights switch themselves off in rain." : ("Lights that switch themselves off in rain or strong wind when uncovered: " + string.Join(", ", list2)))); log.LogInfo((object)($"Server ownership mode can manage {dictionary2.Count} fire type(s)." + ((list4.Count == 0) ? string.Empty : (" Except where their extras apply (MeltsSnow: Deep North; SpreadsFire: Ashlands or fire modifier): " + string.Join(", ", list4) + ".")) + ((list3.Count == 0) ? string.Empty : (" Never (other owner-run parts): " + string.Join(", ", list3) + ".")))); log.LogInfo((object)$"Ward types for 'nearby': {result.WardRadii.Count}."); return result; } private static CatalogItem? TryCreateItem(GameObject prefab, ManualLogSource log) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown try { Fireplace component = prefab.GetComponent<Fireplace>(); if ((Object)(object)component != (Object)null) { return (component.m_infiniteFuel || component.m_secPerFuel <= 0f || component.m_maxFuel <= 0f) ? null : Create(prefab, component.m_name, (ItemKind)0, new FuelItemInfo(component.m_maxFuel, true, component.m_secPerFuel)); } Smelter component2 = prefab.GetComponent<Smelter>(); if ((Object)(object)component2 != (Object)null) { if (component2.m_maxFuel <= 0 || (Object)(object)component2.m_fuelItem == (Object)null) { return null; } bool flag = component2.m_conversion != null && component2.m_conversion.Exists((ItemConversion c) => (Object)(object)c?.m_to != (Object)null); ItemKind kind = (ItemKind)((component2.m_maxOre > 0 || flag) ? 1 : 0); return Create(prefab, component2.m_name, kind, new FuelItemInfo((float)component2.m_maxFuel, false, 0f)); } CookingStation component3 = prefab.GetComponent<CookingStation>(); if ((Object)(object)component3 != (Object)null) { return (!component3.m_useFuel || component3.m_maxFuel <= 0) ? null : Create(prefab, component3.m_name, (ItemKind)1, new FuelItemInfo((float)component3.m_maxFuel, false, 0f)); } } catch (Exception ex) when (((ex is ArgumentException || ex is NullReferenceException) ? 1 : 0) != 0) { log.LogWarning((object)("Skipping prefab '" + ((Object)prefab).name + "': " + ex.Message)); } return null; } private static bool HasOtherOwnerParts(GameObject prefab) { if (!((Object)(object)prefab.GetComponentInChildren<Smelter>() != (Object)null) && !((Object)(object)prefab.GetComponentInChildren<CookingStation>() != (Object)null) && !((Object)(object)prefab.GetComponentInChildren<Container>() != (Object)null) && !((Object)(object)prefab.GetComponentInChildren<Fermenter>() != (Object)null) && !((Object)(object)prefab.GetComponentInChildren<Beehive>() != (Object)null)) { return (Object)(object)prefab.GetComponentInChildren<Ship>() != (Object)null; } return true; } private static FireExtras ExtrasOf(Fireplace fireplace) { return (FireExtras)((((Object)(object)fireplace.m_snowMelter != (Object)null) ? 1 : 0) | ((fireplace.m_igniteInterval > 0f && fireplace.m_igniteCapsuleRadius > 0f && (Object)(object)fireplace.m_firePrefab != (Object)null) ? 2 : 0)); } private static bool IsRainSensitive(GameObject prefab) { Fireplace component = prefab.GetComponent<Fireplace>(); if ((Object)(object)component != (Object)null && component.m_canTurnOff && (Object)(object)component.m_enabledObjectLow != (Object)null) { return (Object)(object)component.m_enabledObjectHigh != (Object)null; } return false; } private static CatalogItem Create(GameObject prefab, string componentName, ItemKind kind, FuelItemInfo fuel) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown Piece component = prefab.GetComponent<Piece>(); string token = (((Object)(object)component != (Object)null && !string.IsNullOrWhiteSpace(component.m_name)) ? component.m_name : componentName); return new CatalogItem(((Object)prefab).name, Localize(token, ((Object)prefab).name), kind, fuel); } private static string Localize(string token, string fallback) { if (string.IsNullOrWhiteSpace(token)) { return fallback; } try { Localization instance = Localization.instance; string text = ((instance != null) ? instance.Localize(token) : null); return string.IsNullOrWhiteSpace(text) ? token : text; } catch (Exception ex) when (((ex is NullReferenceException || ex is InvalidOperationException) ? 1 : 0) != 0) { return token; } } } internal sealed class ServerOwnership(PrefabDiscovery.Result discovery, ISettingsStore store, ManualLogSource log) { private const double LoanSeconds = 30.0; private const double ForwardDelaySeconds = 1.0; private static readonly int AddFuelHash = StringExtensionMethods.GetStableHashCode("RPC_AddFuel"); private static readonly int AddFuelAmountHash = StringExtensionMethods.GetStableHashCode("RPC_AddFuelAmount"); private static readonly int SetFuelAmountHash = StringExtensionMethods.GetStableHashCode("RPC_SetFuelAmount"); private static readonly int ToggleOnHash = StringExtensionMethods.GetStableHashCode("RPC_ToggleOn"); private static readonly Action<ZRoutedRpc, RoutedRPCData> RouteRpc = AccessTools.MethodDelegate<Action<ZRoutedRpc, RoutedRPCData>>(AccessTools.Method(typeof(ZRoutedRpc), "RouteRPC", (Type[])null, (Type[])null), (object)null, true); private readonly LoanBook<ZDOID> _loans = new LoanBook<ZDOID>(); private readonly Dictionary<ZDOID, Biome> _biomes = new Dictionary<ZDOID, Biome>(); private readonly List<(double At, RoutedRPCData Data)> _forwards = new List<(double, RoutedRPCData)>(); public static long ServerId => ZDOMan.GetSessionID(); public bool Enabled => store.General.ServerOwnership; public static bool IsServerSimulated(ZDO zdo) { if (zdo.GetOwner() == ServerId) { return (Object)(object)ZNetScene.instance.FindInstance(zdo) == (Object)null; } return false; } public void Manage(ZDO zdo, double now) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Invalid comparison between Unknown and I4 //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Invalid comparison between Unknown and I4 OwnershipAction val = OwnershipPolicy.Decide(Enabled, IsEligible(zdo), zdo.GetOwner() == ServerId, _loans.IsOnLoan(zdo.m_uid, now)); if ((int)val == 1) { zdo.SetOwner(ServerId); } else if ((int)val == 2) { zdo.SetOwner(0L); } } public bool IsProtected(ZDO zdo) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (Enabled && zdo.GetOwner() == ServerId && IsEligible(zdo)) { return !_loans.IsOnLoan(zdo.m_uid, (double)Time.realtimeSinceStartup); } return false; } public bool IsEligible(ZDO zdo) { //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_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Invalid comparison between Unknown and I4 //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Invalid comparison between Unknown and I4 if (!discovery.OwnershipCandidates.TryGetValue(zdo.GetPrefab(), out var value)) { return false; } if (value == PrefabDiscovery.FireExtras.None) { return true; } Biome val = BiomeOf(zdo); if ((value & PrefabDiscovery.FireExtras.MeltsSnow) != PrefabDiscovery.FireExtras.None && (int)val == 64) { return false; } if ((value & PrefabDiscovery.FireExtras.SpreadsFire) != PrefabDiscovery.FireExtras.None) { if ((int)val != 32) { return !ZoneSystem.instance.GetGlobalKey((GlobalKeys)18); } return false; } return true; } private Biome BiomeOf(ZDO zdo) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (!_biomes.TryGetValue(zdo.m_uid, out var value)) { WorldGenerator instance = WorldGenerator.instance; value = (Biome)((instance != null) ? ((int)instance.GetBiome(zdo.GetPosition())) : 0); _biomes[zdo.m_uid] = value; } return value; } public bool TryHandleRpc(RoutedRPCData data) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown if (((ZDOID)(ref data.m_targetZDO)).IsNone() || data.m_targetPeerID != ServerId) { return false; } ZDO zDO = ZDOMan.instance.GetZDO(data.m_targetZDO); if (zDO == null || !IsServerSimulated(zDO) || !discovery.ByHash.TryGetValue(zDO.GetPrefab(), out CatalogItem value) || !value.Fuel.BurnsOverTime) { return false; } int methodHash = data.m_methodHash; ZPackage val = new ZPackage(data.m_parameters.GetArray()); float num = zDO.GetFloat(ZDOVars.s_fuel, 0f); float maxFuel = value.Fuel.MaxFuel; if (methodHash == AddFuelHash) { zDO.Set(ZDOVars.s_fuel, FireplaceRpc.AddOne(num, maxFuel)); } else if (methodHash == AddFuelAmountHash) { zDO.Set(ZDOVars.s_fuel, FireplaceRpc.AddAmount(num, val.ReadSingle(), maxFuel)); } else if (methodHash == SetFuelAmountHash) { zDO.Set(ZDOVars.s_fuel, FireplaceRpc.SetAmount(val.ReadSingle(), maxFuel)); } else { if (methodHash != ToggleOnHash) { Lend(zDO, data, value.PrefabName); return true; } zDO.Set(ZDOVars.s_state, FireplaceRpc.Toggle(zDO.GetInt(ZDOVars.s_state, 1)), false); } log.LogDebug((object)$"Handled {MethodName(methodHash)} on server-owned {value.PrefabName} from peer {data.m_senderPeerID}."); return true; } public void Tick(double now) { for (int num = _forwards.Count - 1; num >= 0; num--) { if (!(now < _forwards[num].At)) { RoutedRPCData item = _forwards[num].Data; _forwards.RemoveAt(num); try { RouteRpc(ZRoutedRpc.instance, item); } catch (Exception e) { Diagnostics.Error("passing a player's action on to them", e); } } } _loans.Prune(now); } public int ReleaseAll(IEnumerable<ZDO> zdos) { int num = 0; foreach (ZDO zdo in zdos) { try { if (zdo != null && zdo.GetOwner() == ServerId && discovery.OwnershipCandidates.ContainsKey(zdo.GetPrefab())) { zdo.SetOwner(0L); num++; } } catch (Exception e) { Diagnostics.Error("giving a server-owned fire back", e); } } _loans.Clear(); _forwards.Clear(); return num; } private void Lend(ZDO zdo, RoutedRPCData data, string prefabName) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0055: 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_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_0092: Expected O, but got Unknown long senderPeerID = data.m_senderPeerID; float realtimeSinceStartup = Time.realtimeSinceStartup; _loans.Lend(zdo.m_uid, (double)realtimeSinceStartup, 30.0); if (senderPeerID == 0L || senderPeerID == ServerId) { zdo.SetOwner(0L); return; } zdo.SetOwner(senderPeerID); RoutedRPCData item = new RoutedRPCData { m_msgID = data.m_msgID, m_senderPeerID = senderPeerID, m_targetPeerID = senderPeerID, m_targetZDO = data.m_targetZDO, m_methodHash = data.m_methodHash, m_parameters = new ZPackage(data.m_parameters.GetArray()) }; _forwards.Add(((double)realtimeSinceStartup + 1.0, item)); log.LogDebug((object)$"Lent server-owned {prefabName} to peer {senderPeerID} for an action ({data.m_methodHash})."); } private static string MethodName(int hash) { if (hash != AddFuelHash) { if (hash != AddFuelAmountHash) { if (hash != SetFuelAmountHash) { return "switch on/off"; } return "set fuel"; } return "add fuel amount"; } return "add fuel"; } } internal sealed class SyncServer(ConfigSettingsStore store, CommandService commands, ManualLogSource log) { public const string HelloRpc = "Inferno_Hello"; public const string CommandRpc = "Inferno_Command"; public const string SettingsRpc = "Inferno_Settings"; private const float BroadcastIntervalSeconds = 0.5f; private const float HelloCooldownSeconds = 5f; private readonly HashSet<long> _subscribers = new HashSet<long>(); private readonly Dictionary<long, float> _lastHello = new Dictionary<long, float>(); private bool _dirty; private float _nextBroadcast; public void Register(ZRoutedRpc rpc) { rpc.Register<int>("Inferno_Hello", (Action<long, int>)OnHello); rpc.Register<string>("Inferno_Command", (Action<long, string>)OnCommand); commands.SettingsChanged += MarkDirty; } public void Unregister() { commands.SettingsChanged -= MarkDirty; } public void Tick(float now) { if (_dirty && !(now < _nextBroadcast)) { _dirty = false; _nextBroadcast = now + 0.5f; Broadcast(); } } private void MarkDirty() { _dirty = true; } private void OnHello(long sender, int protocolVersion) { try { Hello(sender, protocolVersion); } catch (Exception e) { Diagnostics.Error("answering a client mod's hello", e); } } private void Hello(long sender, int protocolVersion) { ZNetPeer val = Players.FindPeer(sender); float realtimeSinceStartup = Time.realtimeSinceStartup; if (val != null && (!_lastHello.TryGetValue(sender, out var value) || !(realtimeSinceStartup - value < 5f))) { _lastHello[sender] = realtimeSinceStartup; if (protocolVersion != 1) { log.LogWarning((object)$"{Players.FromPeer(val)} has an incompatible Inferno client (protocol {protocolVersion}, server {1}); menu disabled for them."); Players.ShowMessage(val, new <>z__ReadOnlySingleElementList<string>("Your Inferno version doesn't match the server's; the settings menu is disabled. Please update.")); } else { _subscribers.Add(sender); log.LogInfo((object)$"{Players.FromPeer(val)} connected with the Inferno client mod."); Send(val); } } } private void OnCommand(long sender, string text) { try { commands.HandleMenu(sender, text); } catch (Exception e) { Diagnostics.Error("running a settings-menu command", e); } } private void Broadcast() { foreach (long item in new List<long>(_subscribers)) { ZNetPeer val = Players.FindPeer(item); if (val == null) { _subscribers.Remove(item); _lastHello.Remove(item); continue; } try { Send(val); } catch (Exception e) { Diagnostics.Error("sending settings to a client mod", e); } } } private void Send(ZNetPeer peer) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown SettingsMessage val = new SettingsMessage(PermissionPolicy.CanChangeSettings(Players.FromPeer(peer).IsAdmin, store.General.AdminOnly), store.General, store.Snapshot().Items); ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "Inferno_Settings", new object[1] { val.Encode() }); } } }