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 DvergrAstrolabe v0.3.0
plugins/DvergrAstrolabe.dll
Decompiled 4 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("DvergrAstrolabe")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: AssemblyInformationalVersion("0.3.0+011155115563188654dbca33ffaf400a3129dd5c")] [assembly: AssemblyProduct("DvergrAstrolabe")] [assembly: AssemblyTitle("DvergrAstrolabe")] [assembly: AssemblyVersion("0.3.0.0")] [module: RefSafetyRules(11)] 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 DvergrAstrolabe { public enum PauseMode { Off, Ships, Water } public enum BandMode { Auto, Vanilla } internal static class AstrolabeConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<bool> VerboseLogging; public static ConfigEntry<float> DayMinutes; public static ConfigEntry<float> NightMinutes; public static ConfigEntry<bool> HoldSchedule; public static ConfigEntry<float> CatchUpLimit; public static ConfigEntry<float> ClockPushInterval; public static ConfigEntry<PauseMode> PauseFor; public static ConfigEntry<float> PauseRadius; public static ConfigEntry<float> MooredRadius; public static ConfigEntry<float> UnderWaySpeed; public static ConfigEntry<float> CheckInterval; public static ConfigEntry<BandMode> BandDetection; public static ConfigEntry<int> BandProbeSteps; public static void Bind(ConfigFile config) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Expected O, but got Unknown //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Expected O, but got Unknown //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Expected O, but got Unknown //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01c2: Expected O, but got Unknown //IL_01f0: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Expected O, but got Unknown //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_0232: Expected O, but got Unknown //IL_024e: Unknown result type (might be due to invalid IL or missing references) //IL_0258: Expected O, but got Unknown //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_028d: Expected O, but got Unknown Enabled = config.Bind<bool>("1 - General", "Enabled", true, "Master switch. Off = the world clock runs at vanilla speed (21 real minutes of daylight, 9 of night) and nothing is pushed to clients beyond vanilla's own 2-second sync."); VerboseLogging = config.Bind<bool>("1 - General", "VerboseLogging", false, "Log every sunrise and sunset with the day number, how long the segment that just ended really ran against its schedule, and the clock rate that just took effect; also every pause and resume near boats. Off = only the schedule line at startup and after a config reload."); DayMinutes = config.Bind<float>("2 - Day And Night", "DayMinutes", 21f, new ConfigDescription("Real minutes from sunrise to sunset: the whole stretch in which the sun is above the horizon and the game counts as day (EnvMan.IsDay, the 'Day N' banner fires at its start). Vanilla is 21. Nothing else about a day changes: it is still 1800 game-seconds, so crops, smelters, fermenters, raids and the day counter all take the same number of DAYS - a longer setting just makes each of those days take longer on the wall clock.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 180f), Array.Empty<object>())); NightMinutes = config.Bind<float>("2 - Day And Night", "NightMinutes", 9f, new ConfigDescription("Real minutes from sunset to sunrise: dusk, the midnight day-counter tick and dawn. Vanilla is 9 (4.5 either side of midnight). Sleeping still skips to the next sunrise in 12 real seconds regardless of this value.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 180f), Array.Empty<object>())); HoldSchedule = config.Bind<bool>("2 - Day And Night", "HoldSchedule", true, "Hold every sunrise and sunset to its scheduled real time. After the clock had to run at vanilla speed for a while (see [4 - Boats]), the rest of the segment runs faster or slower to land the boundary on time anyway: a 40-minute day absorbs up to 21 minutes of boating, a 5-minute night about 4.5. Also corrects for server hitches. Off = every segment runs at its nominal rate and a pause shifts the schedule by the time it paused."); CatchUpLimit = config.Bind<float>("2 - Day And Night", "CatchUpLimit", 4f, new ConfigDescription("How far from the nominal rate the catch-up may go, as a ratio either side: 4 lets a 0.525x day run anywhere between 0.131x and 2.1x. Higher lands more boundaries on time after long pauses at the cost of a visibly faster or slower sun afterwards. 1 = never deviate (same as HoldSchedule off).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 20f), Array.Empty<object>())); ClockPushInterval = config.Bind<float>("3 - Client Sync", "ClockPushInterval", 0.5f, new ConfigDescription("Seconds between extra pushes of the world clock to connected clients. Vanilla pushes every 2 seconds, and between pushes an unmodded client runs its own copy of the clock at 1x, so each push applies a correction of (interval x |rate - 1|) game-seconds; the sun moves 0.2 degrees per game-second and the client smooths the sun and the rendered water over about 2 seconds. At 0.5 the correction stays under a degree for any rate up to 10x (a 2-minute day). Costs one tiny packet per client per interval. Set 2 to add no extra pushes at all. The water surface boats float on is NOT smoothed, whatever this is set to - see [4 - Boats].", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 2f), Array.Empty<object>())); PauseFor = config.Bind<PauseMode>("4 - Boats", "PauseFor", PauseMode.Ships, new ConfigDescription("While the schedule runs at any rate other than 1x, every clock push moves an unmodded client's physics water surface in a single step, and a boat with anyone aboard takes vanilla's water-impact damage for it (10 per hit, up to thirty hits a minute). So the clock runs at vanilla speed while a player is near a boat; the switch is seamless and, with HoldSchedule on, the rest of the segment absorbs the paused time. Ships = pause while any player is within PauseRadius of a boat under way or MooredRadius of one at rest (every vanilla boat and any modded one). Water = also pause while any player is over water, which spares swimmers, fishing floats and floating items the bobbing but pauses far more often on a coastal server. Off = never pause; boats take damage under sail while the schedule is active.", (AcceptableValueBase)null, Array.Empty<object>())); PauseRadius = config.Bind<float>("4 - Boats", "PauseRadius", 64f, new ConfigDescription("Metres around each player in which a boat UNDER WAY pauses the schedule. A sailing player's reported position trails the hull by a couple of seconds, so keep this generous.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(8f, 256f), Array.Empty<object>())); MooredRadius = config.Bind<float>("4 - Boats", "MooredRadius", 24f, new ConfigDescription("The same for a boat AT REST. A player only takes the hits while aboard, so this only needs to cover the deck, and it keeps a boat docked beside your base from pausing the schedule whenever anyone is home.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(4f, 256f), Array.Empty<object>())); UnderWaySpeed = config.Bind<float>("4 - Boats", "UnderWaySpeed", 2f, new ConfigDescription("Metres per second a boat must be moving, measured between two checks, to count as under way and use PauseRadius instead of MooredRadius. A moored hull drifts far slower than this; a rowed Karve is faster.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 20f), Array.Empty<object>())); CheckInterval = config.Bind<float>("4 - Boats", "CheckInterval", 0.5f, new ConfigDescription("Seconds between boat checks. Each check runs one sector query around every connected player, which is cheap; the schedule reacts to a boat within this delay.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f), Array.Empty<object>())); BandDetection = config.Bind<BandMode>("5 - Compatibility", "BandDetection", BandMode.Auto, new ConfigDescription("Where sunrise and sunset fall in the game day. Auto = read them from the running game by sampling EnvMan.RescaleDayFraction, so a mod that moves them (Gamma of Night Lights' day/night cycle, or anything else that patches that method) is followed: Astrolabe paces the daylight and the night that mod defines, and DayMinutes / NightMinutes mean that daylight and that night. The day length itself (EnvMan.m_dayLengthSec) is read live either way. Vanilla = assume the vanilla knees (raw day-fraction 0.15 and 0.85: 21 + 9 minutes of an 1800-second day) whatever other mods do; the escape hatch if detection ever misreads a future game build. A detection that fails falls back to vanilla by itself and logs an error.", (AcceptableValueBase)null, Array.Empty<object>())); BandProbeSteps = config.Bind<int>("5 - Compatibility", "BandProbeSteps", 1000, new ConfigDescription("How finely the day is sampled when detecting the daylight band: sunrise and sunset are located to within 1/steps of a day (1.8 game-seconds at 1000 on an 1800-second day), and a mod that sets the night in whole percent lands exactly on the grid at 1000. Each detection makes that many calls of one small game function, at startup, after a config change, every 5 seconds while the world is loaded and at every sunrise and sunset.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(100, 10000), Array.Empty<object>())); } } public static class BandProbe { public const float DayBeginsAt = 0.25f; public const float DayEndsAt = 0.75f; public const int MinSteps = 100; public const int MaxSteps = 10000; public const int DefaultSteps = 1000; private const float Tolerance = 1E-05f; private const float Slack = 1E-06f; public static bool Find(Func<float, float> rescale, int steps, out double sunriseFraction, out double sunsetFraction, out string? failure) { sunriseFraction = 0.0; sunsetFraction = 0.0; failure = null; steps = Math.Max(100, Math.Min(10000, steps)); int num = -1; int num2 = -1; float num3 = float.NegativeInfinity; for (int i = 0; i <= steps; i++) { float num4 = (float)i / (float)steps; float num5 = rescale(num4); if (float.IsNaN(num5) || float.IsInfinity(num5)) { failure = $"RescaleDayFraction({num4:0.###}) returned {num5}"; return false; } if (num5 < num3 - 1E-06f) { failure = $"RescaleDayFraction is not monotonic ({num3:0.####} at raw {(double)(i - 1) / (double)steps:0.###}, then {num5:0.####} at raw {num4:0.###})"; return false; } num3 = num5; if (num < 0 && num5 >= 0.24999f) { num = i; } if (num5 <= 0.75001f) { num2 = i; } } if (num < 0) { failure = "RescaleDayFraction never reaches 0.25, so the game would never count day"; return false; } if (num2 < 0) { failure = "RescaleDayFraction starts above 0.75, so the game would never count day"; return false; } sunriseFraction = (double)num / (double)steps; sunsetFraction = (double)num2 / (double)steps; if (sunsetFraction <= sunriseFraction) { failure = $"no daylight: sunset (raw {sunsetFraction:0.###}) is not after sunrise (raw {sunriseFraction:0.###})"; return false; } if (sunsetFraction - sunriseFraction >= 1.0) { failure = "no night: daylight spans the whole day"; return false; } return true; } } internal static class Astrolabe { public static readonly ClockSchedule Schedule = new ClockSchedule(); public const double SlowRateFloor = 0.2; public const double SmoothSunDegrees = 1.0; public const double SunDegreesPerGameSecond = 0.2; private static bool _loggedFirstTick; private static bool _loggedFailure; private static bool _notedMovedBand; public static ClockRate Rate => Schedule.Rate; public static double CurrentRate => Schedule.CurrentRate; public static bool HasFailed => _loggedFailure; public static void ApplySettings(string reason) { EnvMan instance = EnvMan.instance; if ((Object)(object)instance != (Object)null) { DayCycle.Refresh(instance, 0f, force: true); } ConfigureRate(); if (!AstrolabeConfig.Enabled.Value) { Log.Always($"disabled ({reason}): world clock runs at vanilla speed, {Rate.UnpacedDayMinutes:0.#} min day + {Rate.UnpacedNightMinutes:0.#} min night."); return; } float value = AstrolabeConfig.ClockPushInterval.Value; string text = ((value >= 2f) ? "vanilla 2 s only" : $"every {value:0.##} s (plus vanilla's 2 s)"); string text2 = ((Schedule.Hold && Schedule.CatchUpLimit > 1.0) ? $"each sunrise and sunset is held to its scheduled time, catching up at up to {Schedule.CatchUpLimit:0.#}x the nominal rate after a pause" : "each segment runs at its nominal rate, so a pause shifts the schedule"); Log.Always($"schedule ({reason}): daylight {Rate.DayMinutes:0.##} real min at {Rate.DayRate:0.###}x, night {Rate.NightMinutes:0.##} real min at {Rate.NightRate:0.###}x; one full day = {Rate.DayMinutes + Rate.NightMinutes:0.##} real min ({Rate.DayLengthSeconds / 60.0:0.#} at vanilla speed); {DayCycle.Describe(Rate)}; {text2}; clock pushed to clients {text}; {ShipGuard.Describe()}."); if (DayCycle.Read) { ReportEnvelope(); } } private static void ConfigureRate() { Rate.Configure(AstrolabeConfig.DayMinutes.Value, AstrolabeConfig.NightMinutes.Value, DayCycle.DayLengthSeconds, DayCycle.SunriseFraction, DayCycle.SunsetFraction); Schedule.Hold = AstrolabeConfig.HoldSchedule.Value; Schedule.CatchUpLimit = AstrolabeConfig.CatchUpLimit.Value; Schedule.Forget(); } private static void OnCycleChanged(string reason) { ConfigureRate(); bool value = AstrolabeConfig.Enabled.Value; string text = (value ? $"daylight runs at {Rate.DayRate:0.###}x for {Rate.DayMinutes:0.##} real min and night at {Rate.NightRate:0.###}x for {Rate.NightMinutes:0.##}" : "the schedule is disabled, so both run at vanilla speed"); Log.Always($"day cycle ({reason}): {Rate.DayLengthSeconds:0} game-s per day, {DayCycle.Describe(Rate)}; {text}."); if (value) { ReportEnvelope(); } } private static void ReportEnvelope() { if (DayCycle.Read) { string text = (DayCycle.Detected ? $"; EnvMan.RescaleDayFraction sampled in {DayCycle.Steps} steps" : ""); Log.Info($"daylight band detail: sunrise at raw day-fraction {Rate.SunriseFraction:0.####}, sunset at {Rate.SunsetFraction:0.####} (vanilla 0.15 and 0.85), {DayCycle.Status}{text}."); } double num = AstrolabeConfig.ClockPushInterval.Value; double fast = 1.0 + 1.0 / (0.2 * num); WarnRate("daylight", "DayMinutes", Rate.DayMinutes, Rate.DayRate, Rate.DayGameSeconds, num, fast); WarnRate("night", "NightMinutes", Rate.NightMinutes, Rate.NightRate, Rate.NightGameSeconds, num, fast); if (DayCycle.Detected && !Rate.IsVanillaBand && !_notedMovedBand) { _notedMovedBand = true; Log.Always($"note: sunrise and sunset in this world sit at raw day-fraction {Rate.SunriseFraction:0.###} and {Rate.SunsetFraction:0.###} instead of vanilla's 0.15 and 0.85, so another mod shapes the cycle (Gamma of Night Lights' day/night cycle does exactly this) and Astrolabe paces the band it defines. That mod has to be on every client as well, or the night the server paces is not the night players see; Astrolabe itself still needs nothing on clients."); } } private static void WarnRate(string segment, string setting, double minutes, double rate, double gameSeconds, double push, double fast) { if (rate <= 0.2) { Log.Warn($"{setting} = {minutes:0.##} runs {segment} at {rate:0.###}x, at or under the {0.2:0.#}x floor: below it a client-owned fire, beehive or sap collector can drop a fraction of a second of progress at each clock correction (README, fine print). {segment} is {gameSeconds:0} game-s in this world, so keep {setting} under {gameSeconds / 60.0 / 0.2:0.#} to stay above the floor, or accept the drift."); } else if (rate > fast) { Log.Warn($"{setting} = {minutes:0.##} runs {segment} at {rate:0.###}x: with ClockPushInterval = {push:0.##} s each clock push moves the sun {0.2 * push * (rate - 1.0):0.#} degrees in one step, which no longer looks smooth (over {fast:0.#}x at this interval). Lower ClockPushInterval or keep {setting} at {gameSeconds / 60.0 / fast:0.#} or more."); } } public static double? ScaledAdvance(ZNet net, float dt) { EnvMan instance = EnvMan.instance; if ((Object)(object)instance != (Object)null) { bool flag = !DayCycle.Read; if (DayCycle.Refresh(instance, dt, force: false)) { OnCycleChanged(flag ? "read from EnvMan" : "changed"); } } if (!AstrolabeConfig.Enabled.Value || dt <= 0f) { Schedule.Idle(); return null; } double timeSeconds = net.GetTimeSeconds(); if (!_loggedFirstTick) { _loggedFirstTick = true; Log.Always(string.Format("clock patch live: EnvMan {0}, day {1}, raw day-fraction {2:0.000} ({3}), {4} player(s) connected.", ((Object)(object)instance != (Object)null) ? "found" : $"NOT found (sleep-skip detection unavailable, day length assumed {1800.0:0})", Rate.DayAt(timeSeconds), ClockRate.RawFraction(timeSeconds, Rate.DayLengthSeconds), Rate.IsSunUpAt(timeSeconds) ? "daylight" : "night", net.GetNrOfPlayers())); } if ((Object)(object)instance != (Object)null && instance.IsTimeSkipping()) { Schedule.Forget(); Schedule.Idle(); return null; } if (Schedule.Observe(timeSeconds, out var crossing)) { LogCrossing(crossing, timeSeconds); if ((Object)(object)instance != (Object)null && DayCycle.Refresh(instance, 0f, force: true)) { OnCycleChanged(crossing.SunUp ? "changed at sunrise" : "changed at sunset"); } } bool running = net.GetNrOfPlayers() > 0; bool active = ShipGuard.Active; double value = Schedule.Advance(timeSeconds, dt, running, active); if (!active) { return value; } return null; } private static void LogCrossing(ClockSchedule.Crossing c, double now) { string text = (c.SunUp ? "night" : "daylight"); string text2 = ((c.PausedSeconds >= 1.0) ? $", {c.PausedSeconds / 60.0:0.0} of them at vanilla speed near boats" : ""); string text3 = (c.Measured ? $"{text} ran {c.RanSeconds / 60.0:0.0} real min ({c.ScheduledSeconds / 60.0:0.##} scheduled{text2}); " : ""); string text4 = (ShipGuard.Active ? ", paused near boats right now" : ""); if (c.SunUp) { Log.Info($"sunrise, day {Rate.DayAt(now)}: {text3}clock at {Rate.DayRate:0.###}x for the next {Rate.DayMinutes:0.##} real min{text4}."); } else { Log.Info($"sunset, day {Rate.DayAt(now)}: {text3}clock at {Rate.NightRate:0.###}x for the next {Rate.NightMinutes:0.##} real min{text4}."); } } public static void ReportFailure(Exception ex) { if (!_loggedFailure) { _loggedFailure = true; Log.Error($"clock tick failed, leaving vanilla dt untouched from now on until restart: {ex}"); } } } [HarmonyPatch(typeof(ZNet), "UpdateNetTime")] internal static class UpdateNetTimePatch { [HarmonyPrefix] private static void Prefix(ZNet __instance, ref float dt) { if (Astrolabe.HasFailed || !__instance.IsServer()) { return; } try { double? num = Astrolabe.ScaledAdvance(__instance, dt); if (num.HasValue) { dt = (float)num.Value; } } catch (Exception ex) { Astrolabe.ReportFailure(ex); } } } internal static class ClockPush { public const float VanillaPushIntervalSeconds = 2f; private static readonly MethodInfo? SendNetTime = AccessTools.Method(typeof(ZNet), "SendNetTime", (Type[])null, (Type[])null); private static float _timer; private static bool _warned; public static void Tick(float realDt) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !AstrolabeConfig.Enabled.Value || ShipGuard.Active || Math.Abs(Astrolabe.CurrentRate - 1.0) < 0.005) { _timer = 0f; return; } float value = AstrolabeConfig.ClockPushInterval.Value; if (value >= 2f) { _timer = 0f; return; } _timer += realDt; if (_timer < value) { return; } _timer = 0f; if (SendNetTime == null) { WarnOnce("ZNet.SendNetTime not found in this game build - extra clock pushes disabled, vanilla's 2 s sync still applies."); return; } try { SendNetTime.Invoke(instance, null); } catch (Exception ex) { WarnOnce("extra clock push failed (" + ex.GetType().Name + ": " + ex.Message + ") - vanilla's 2 s sync still applies."); } } private static void WarnOnce(string message) { if (!_warned) { _warned = true; Log.Warn(message); } } } public sealed class ClockRate { public const double VanillaSunriseFraction = 0.15; public const double VanillaSunsetFraction = 0.85; public const double VanillaDayLengthSeconds = 1800.0; public const double VanillaDayMinutes = 21.0; public const double VanillaNightMinutes = 9.0; public const double AbsoluteMinRate = 0.02; public const double AbsoluteMaxRate = 60.0; private const double BoundaryNudgeSeconds = 0.0001; private const int MaxSplitsPerStep = 8; private const double MinSegmentMinutes = 1.0 / 60.0; public double DayLengthSeconds { get; private set; } = 1800.0; public double SunriseFraction { get; private set; } = 0.15; public double SunsetFraction { get; private set; } = 0.85; public double DayMinutes { get; private set; } = 21.0; public double NightMinutes { get; private set; } = 9.0; public double DayRate { get; private set; } = 1.0; public double NightRate { get; private set; } = 1.0; public double DayGameSeconds => (SunsetFraction - SunriseFraction) * DayLengthSeconds; public double NightGameSeconds => DayLengthSeconds - DayGameSeconds; public double UnpacedDayMinutes => DayGameSeconds / 60.0; public double UnpacedNightMinutes => NightGameSeconds / 60.0; public bool IsVanilla { get { if (Math.Abs(DayRate - 1.0) < 1E-09) { return Math.Abs(NightRate - 1.0) < 1E-09; } return false; } } public bool IsVanillaBand => IsVanillaBandValues(SunriseFraction, SunsetFraction); public static bool IsVanillaBandValues(double sunriseFraction, double sunsetFraction) { if (Math.Abs(sunriseFraction - 0.15) < 1E-09) { return Math.Abs(sunsetFraction - 0.85) < 1E-09; } return false; } public static bool IsUsableBand(double sunriseFraction, double sunsetFraction) { if (sunriseFraction >= 0.0 && sunsetFraction <= 1.0 && sunsetFraction > sunriseFraction) { return sunsetFraction - sunriseFraction < 1.0; } return false; } public void Configure(double dayMinutes, double nightMinutes, double dayLengthSeconds, double sunriseFraction, double sunsetFraction) { DayLengthSeconds = ((dayLengthSeconds > 1.0) ? dayLengthSeconds : 1800.0); if (IsUsableBand(sunriseFraction, sunsetFraction)) { SunriseFraction = sunriseFraction; SunsetFraction = sunsetFraction; } else { SunriseFraction = 0.15; SunsetFraction = 0.85; } DayMinutes = Math.Max(dayMinutes, 1.0 / 60.0); NightMinutes = Math.Max(nightMinutes, 1.0 / 60.0); DayRate = DayGameSeconds / (DayMinutes * 60.0); NightRate = NightGameSeconds / (NightMinutes * 60.0); } public static double RawFraction(double gameSeconds, double dayLengthSeconds) { double num = gameSeconds % dayLengthSeconds / dayLengthSeconds; if (num < 0.0) { num += 1.0; } return num; } public bool IsSunUp(double rawFraction) { if (rawFraction >= SunriseFraction) { return rawFraction < SunsetFraction; } return false; } public bool IsSunUpAt(double gameSeconds) { return IsSunUp(RawFraction(gameSeconds, DayLengthSeconds)); } public double NominalRate(bool sunUp) { if (!sunUp) { return NightRate; } return DayRate; } public double RateAt(double gameSeconds) { return NominalRate(IsSunUpAt(gameSeconds)); } public int DayAt(double gameSeconds) { return (int)(gameSeconds / DayLengthSeconds); } public double SegmentGameSeconds(bool sunUp) { if (!sunUp) { return NightGameSeconds; } return DayGameSeconds; } public double ScheduledSeconds(bool sunUp) { return (sunUp ? DayMinutes : NightMinutes) * 60.0; } public double GameSecondsToBoundary(double gameSeconds) { double num = RawFraction(gameSeconds, DayLengthSeconds); return ((IsSunUp(num) ? SunsetFraction : ((num < SunriseFraction) ? SunriseFraction : (1.0 + SunriseFraction))) - num) * DayLengthSeconds; } public double RequiredRate(bool sunUp, double remainingGameSeconds, double remainingRealSeconds, bool hold, double catchUpLimit) { double num = NominalRate(sunUp); if (!hold || catchUpLimit <= 1.0) { return num; } double num2 = Math.Max(0.02, num / catchUpLimit); double num3 = Math.Min(60.0, num * catchUpLimit); if (num3 < num2) { return num; } if (remainingRealSeconds <= 0.0) { return num3; } double val = remainingGameSeconds / remainingRealSeconds; return Math.Min(num3, Math.Max(num2, val)); } public double Advance(double gameSeconds, double realSeconds) { return Advance(gameSeconds, realSeconds, RateAt(gameSeconds)); } public double Advance(double gameSeconds, double realSeconds, double rate) { if (realSeconds <= 0.0 || rate <= 0.0) { return 0.0; } double num = 0.0; double num2 = realSeconds; for (int i = 0; i < 8; i++) { if (!(num2 > 0.0)) { break; } double num3 = RawFraction(gameSeconds + num, DayLengthSeconds); bool flag = IsSunUp(num3); double num4 = ((flag ? SunsetFraction : ((num3 < SunriseFraction) ? SunriseFraction : 1.0)) - num3) * DayLengthSeconds; double num5 = num2 * rate; if (num5 <= num4) { return num + num5; } num += num4 + 0.0001; num2 -= num4 / rate; bool flag2 = IsSunUp(RawFraction(gameSeconds + num, DayLengthSeconds)); if (flag2 != flag) { rate = NominalRate(flag2); } } return num; } } public sealed class ClockSchedule { public struct Crossing { public bool SunUp; public bool Measured; public double RanSeconds; public double PausedSeconds; public double ScheduledSeconds; } private bool _haveSegment; private bool _sunUp; private bool _measured; private double _budget; private double _run; private double _paused; public ClockRate Rate { get; } = new ClockRate(); public bool Hold { get; set; } = true; public double CatchUpLimit { get; set; } = 4.0; public double CurrentRate { get; private set; } = 1.0; public double BudgetRemaining => _budget; public void Forget() { _haveSegment = false; } public void Idle() { CurrentRate = 1.0; } public bool Observe(double now, out Crossing crossing) { crossing = default(Crossing); bool flag = Rate.IsSunUpAt(now); if (_haveSegment && flag == _sunUp) { return false; } bool flag2 = !_haveSegment; _haveSegment = true; _sunUp = flag; if (!flag2) { crossing = new Crossing { SunUp = flag, Measured = _measured, RanSeconds = _run, PausedSeconds = _paused, ScheduledSeconds = Rate.ScheduledSeconds(!flag) }; } double num = Rate.ScheduledSeconds(flag); _budget = (flag2 ? (num * Rate.GameSecondsToBoundary(now) / Rate.SegmentGameSeconds(flag)) : num); _measured = !flag2; _run = 0.0; _paused = 0.0; return !flag2; } public double Advance(double now, double dt, bool running, bool paused) { if (dt <= 0.0) { return 0.0; } if (running) { _run += dt; _budget -= dt; if (paused) { _paused += dt; } } if (paused) { CurrentRate = 1.0; return dt; } double rate = (CurrentRate = Rate.RequiredRate(_sunUp, Rate.GameSecondsToBoundary(now), _budget, Hold, CatchUpLimit)); return Rate.Advance(now, dt, rate); } } internal static class DayCycle { public const float RefreshIntervalSeconds = 5f; private static readonly MethodInfo? RescaleDayFraction = AccessTools.Method(typeof(EnvMan), "RescaleDayFraction", new Type[1] { typeof(float) }, (Type[])null); private static readonly object[] InvokeArgs = new object[1]; private static float _timer = float.MaxValue; private static string? _loggedFailure; public static double DayLengthSeconds { get; private set; } = 1800.0; public static double SunriseFraction { get; private set; } = 0.15; public static double SunsetFraction { get; private set; } = 0.85; public static bool Read { get; private set; } public static BandMode Mode { get; private set; } = BandMode.Auto; public static int Steps { get; private set; } = 1000; public static string? Failure { get; private set; } public static bool Detected { get { if (Read && Mode == BandMode.Auto) { return Failure == null; } return false; } } public static bool IsVanillaBand => ClockRate.IsVanillaBandValues(SunriseFraction, SunsetFraction); public static string Status { get { if (!Read) { return "vanilla assumed until the world loads"; } if (Mode == BandMode.Vanilla) { return "vanilla forced by BandDetection"; } if (Failure != null) { return "vanilla assumed because detection failed"; } if (!IsVanillaBand) { return "sunrise and sunset moved by another mod"; } return "vanilla"; } } public static string Describe(ClockRate rate) { double num = ((rate.DayLengthSeconds > 0.0) ? (100.0 * rate.DayGameSeconds / rate.DayLengthSeconds) : 0.0); return $"daylight band {rate.DayGameSeconds:0} of {rate.DayLengthSeconds:0} game-s ({num:0.#}%, {Status})"; } public static bool Refresh(EnvMan env, float realDt, bool force) { _timer += realDt; if (!force && _timer < 5f) { return false; } _timer = 0f; double num = env.m_dayLengthSec; BandMode value = AstrolabeConfig.BandDetection.Value; int steps = Math.Max(100, Math.Min(10000, AstrolabeConfig.BandProbeSteps.Value)); double sunrise = 0.15; double sunset = 0.85; string failure = null; if (value == BandMode.Auto && !TryProbe(env, steps, out sunrise, out sunset, out failure)) { sunrise = 0.15; sunset = 0.85; } bool result = !Read || value != Mode || Math.Abs(num - DayLengthSeconds) > 0.5 || sunrise != SunriseFraction || sunset != SunsetFraction; Read = true; Mode = value; Steps = steps; DayLengthSeconds = num; SunriseFraction = sunrise; SunsetFraction = sunset; Failure = failure; if (failure == null) { _loggedFailure = null; } else if (failure != _loggedFailure) { _loggedFailure = failure; Log.Error("could not detect the daylight band from EnvMan.RescaleDayFraction: " + failure + ". Assuming vanilla sunrise and sunset (raw day-fraction 0.15 and 0.85); set BandDetection = Vanilla to stop probing."); } return result; } private static bool TryProbe(EnvMan env, int steps, out double sunrise, out double sunset, out string? failure) { sunrise = 0.0; sunset = 0.0; MethodInfo method = RescaleDayFraction; if (method == null) { failure = "EnvMan.RescaleDayFraction(float) is not in this game build"; return false; } try { return BandProbe.Find(delegate(float raw) { InvokeArgs[0] = raw; return (float)method.Invoke(env, InvokeArgs); }, steps, out sunrise, out sunset, out failure); } catch (Exception ex) { Exception ex2 = ((ex is TargetInvocationException { InnerException: not null } ex3) ? ex3.InnerException : ex); failure = ex2.GetType().Name + ": " + ex2.Message; return false; } } } internal static class Log { private static ManualLogSource? _source; public static void Init(ManualLogSource source) { _source = source; } public static void Info(string message) { if (AstrolabeConfig.VerboseLogging != null && AstrolabeConfig.VerboseLogging.Value) { ManualLogSource? source = _source; if (source != null) { source.LogInfo((object)message); } } } public static void Always(string message) { ManualLogSource? source = _source; if (source != null) { source.LogMessage((object)message); } } public static void Warn(string message) { ManualLogSource? source = _source; if (source != null) { source.LogWarning((object)message); } } public static void Error(string message) { ManualLogSource? source = _source; if (source != null) { source.LogError((object)message); } } } [BepInPlugin("wubarrk.dvergrastrolabe", "DvergrAstrolabe", "0.3.0")] public class DvergrAstrolabePlugin : BaseUnityPlugin { public const string ModGUID = "wubarrk.dvergrastrolabe"; public const string ModName = "DvergrAstrolabe"; public const string ModVersion = "0.3.0"; private const float ConfigPollInterval = 5f; private readonly Harmony _harmony = new Harmony("wubarrk.dvergrastrolabe"); private float _configPollTimer; private DateTime _configStamp; public static DvergrAstrolabePlugin Instance { get; private set; } private void Awake() { Instance = this; Log.Init(((BaseUnityPlugin)this).Logger); AstrolabeConfig.Bind(((BaseUnityPlugin)this).Config); _configStamp = ReadConfigStamp(); Log.Always("Starting DvergrAstrolabe v0.3.0 (server-side only, built for Valheim 1.0+)..."); try { _harmony.PatchAll(typeof(UpdateNetTimePatch)); } catch (Exception ex) { Log.Error("could not patch ZNet.UpdateNetTime (" + ex.GetType().Name + ": " + ex.Message + ") - the world clock stays at vanilla speed."); return; } Astrolabe.ApplySettings("startup"); Log.Always("DvergrAstrolabe initialized."); } private void Update() { PollConfigFile(); try { ShipGuard.Tick(Time.deltaTime); ClockPush.Tick(Time.deltaTime); } catch (Exception arg) { Log.Error($"tick failed: {arg}"); } } private void OnDestroy() { try { _harmony.UnpatchSelf(); } catch { } } private void PollConfigFile() { _configPollTimer += Time.deltaTime; if (_configPollTimer < 5f) { return; } _configPollTimer = 0f; try { if (!(ReadConfigStamp() == _configStamp)) { bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; try { ((BaseUnityPlugin)this).Config.Reload(); } finally { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; } _configStamp = ReadConfigStamp(); Astrolabe.ApplySettings("config file changed on disk"); } } catch (Exception ex) { Log.Warn("config reload failed: " + ex.GetType().Name + ": " + ex.Message); } } private DateTime ReadConfigStamp() { try { return File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) ? File.GetLastWriteTimeUtc(((BaseUnityPlugin)this).Config.ConfigFilePath) : DateTime.MinValue; } catch { return DateTime.MinValue; } } } internal static class ShipGuard { private const float ZoneSize = 64f; private static readonly List<ZDO> Scratch = new List<ZDO>(); private static readonly Dictionary<int, string?> ShipNames = new Dictionary<int, string>(); private static readonly Dictionary<ZDOID, Vector3> LastShipPos = new Dictionary<ZDOID, Vector3>(); private static readonly Dictionary<ZDOID, bool> UnderWayThisCheck = new Dictionary<ZDOID, bool>(); private static float _timer = float.MaxValue; private static bool _active; private static bool _failed; private static bool _selfTested; private static float _activeSince; public static bool Active { get { if (!_active) { return _failed; } return true; } } public static string Describe() { return AstrolabeConfig.PauseFor.Value switch { PauseMode.Off => "never pauses near boats (PauseFor = Off): boats take water-impact damage under sail while the schedule is active", PauseMode.Water => $"pauses at vanilla speed while any player is over water, within {AstrolabeConfig.PauseRadius.Value:0} m of a boat under way or {AstrolabeConfig.MooredRadius.Value:0} m of one at rest", _ => $"pauses at vanilla speed while any player is within {AstrolabeConfig.PauseRadius.Value:0} m of a boat under way or {AstrolabeConfig.MooredRadius.Value:0} m of one at rest", }; } public static void Tick(float realDt) { _timer += realDt; if (_timer < AstrolabeConfig.CheckInterval.Value || _failed) { return; } _timer = 0f; bool flag = false; string reason = ""; try { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && instance.IsServer() && AstrolabeConfig.Enabled.Value && AstrolabeConfig.PauseFor.Value != PauseMode.Off) { if (!_selfTested) { SelfTest(); } UnderWayThisCheck.Clear(); flag = AnyPlayerNear(instance, out reason); } } catch (Exception arg) { _failed = true; Log.Error($"boat guard failed; the clock stays at vanilla speed until restart so boats stay safe: {arg}"); return; } if (flag != _active) { _active = flag; if (flag) { _activeSince = Time.realtimeSinceStartup; Log.Info("schedule paused, clock at vanilla speed: " + reason + "."); } else { Log.Info($"schedule resumed after {(Time.realtimeSinceStartup - _activeSince) / 60f:0.0} min at vanilla speed."); } } } private static void SelfTest() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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) ZDOMan instance = ZDOMan.instance; ZNetScene instance2 = ZNetScene.instance; if (instance == null || (Object)(object)instance2 == (Object)null || (Object)(object)ZoneSystem.instance == (Object)null) { return; } _selfTested = true; Scratch.Clear(); instance.FindSectorObjects(ZoneSystem.GetZone(Vector3.zero), new SimulationDistance(1, 0, true), Scratch, (List<ZDO>)null); int num = 0; for (int i = 0; i < Scratch.Count; i++) { if (Scratch[i] != null && IsShip(Scratch[i].GetPrefab(), instance2, out string _)) { num++; } } Log.Always($"boat guard ready: sector query OK ({Scratch.Count} objects, {num} boat(s) within a zone of the world origin)."); Scratch.Clear(); } private static bool AnyPlayerNear(ZNet net, out string reason) { //IL_00a7: 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) reason = ""; PauseMode value = AstrolabeConfig.PauseFor.Value; float radius = Mathf.Max(AstrolabeConfig.PauseRadius.Value, AstrolabeConfig.MooredRadius.Value); List<ZNetPeer> peers = net.GetPeers(); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && val.IsReady()) { string who = (string.IsNullOrEmpty(val.m_playerName) ? "a player" : val.m_playerName); if (Check(val.GetRefPos(), who, value, radius, out reason)) { return true; } } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return Check(((Component)localPlayer).transform.position, "the host", value, radius, out reason); } return false; } private static bool Check(Vector3 pos, string who, PauseMode mode, float radius, out string reason) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: 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) reason = ""; if (mode == PauseMode.Water && IsOverWater(pos)) { reason = $"{who} is over water at ({pos.x:0}, {pos.z:0})"; return true; } float distance; bool underWay; string text = NearestShip(pos, radius, out distance, out underWay); if (text == null) { return false; } reason = string.Format("{0} is {1:0} m from a {2} {3}", who, distance, text, underWay ? "under way" : "at rest"); return true; } private static bool IsOverWater(Vector3 pos) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) WorldGenerator instance = WorldGenerator.instance; ZoneSystem instance2 = ZoneSystem.instance; if (instance != null && (Object)(object)instance2 != (Object)null) { return instance.GetHeight(pos.x, pos.z) < instance2.m_waterLevel; } return false; } private static string? NearestShip(Vector3 pos, float radius, out float distance, out bool underWay) { //IL_004f: 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_0058: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) distance = 0f; underWay = false; ZDOMan instance = ZDOMan.instance; ZNetScene instance2 = ZNetScene.instance; if (instance == null || (Object)(object)instance2 == (Object)null || (Object)(object)ZoneSystem.instance == (Object)null) { return null; } int num = Mathf.Max(1, Mathf.CeilToInt(radius / 64f)); Scratch.Clear(); instance.FindSectorObjects(ZoneSystem.GetZone(pos), new SimulationDistance(num, 0, true), Scratch, (List<ZDO>)null); string result = null; float num2 = radius * radius; float value = AstrolabeConfig.MooredRadius.Value; float value2 = AstrolabeConfig.PauseRadius.Value; for (int i = 0; i < Scratch.Count; i++) { ZDO val = Scratch[i]; if (val == null || !IsShip(val.GetPrefab(), instance2, out string name)) { continue; } Vector3 position = val.GetPosition(); if (!UnderWayThisCheck.TryGetValue(val.m_uid, out var value3)) { if (LastShipPos.TryGetValue(val.m_uid, out var value4)) { float num3 = position.x - value4.x; float num4 = position.z - value4.z; float num5 = AstrolabeConfig.UnderWaySpeed.Value * AstrolabeConfig.CheckInterval.Value; value3 = num3 * num3 + num4 * num4 > num5 * num5; } LastShipPos[val.m_uid] = position; UnderWayThisCheck[val.m_uid] = value3; } float num6 = (value3 ? value2 : value); float num7 = position.x - pos.x; float num8 = position.z - pos.z; float num9 = num7 * num7 + num8 * num8; if (num9 <= num6 * num6 && num9 <= num2) { num2 = num9; result = name; underWay = value3; } } Scratch.Clear(); distance = Mathf.Sqrt(num2); return result; } private static bool IsShip(int prefabHash, ZNetScene scene, out string? name) { if (!ShipNames.TryGetValue(prefabHash, out name)) { GameObject prefab = scene.GetPrefab(prefabHash); name = (((Object)(object)prefab != (Object)null && (Object)(object)prefab.GetComponent<Ship>() != (Object)null) ? ((Object)prefab).name : null); ShipNames[prefabHash] = name; } return name != null; } } }