using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicWorldEngine.Contracts; using RunicWorldEngine.Core; using RunicWorldEngine.Integration; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic World Engine")] [assembly: AssemblyDescription("Bounded Valheim world-state observability and safe save smoothing")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic World Engine")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 RunicWorldEngine { internal static class WorldEngineConfig { internal static ConfigEntry Enabled { get; private set; } internal static ConfigEntry LogPeriodicSummary { get; private set; } internal static ConfigEntry SummaryIntervalSeconds { get; private set; } internal static ConfigEntry SmoothWorldSaves { get; private set; } internal static ConfigEntry SaveFrameBudgetMilliseconds { get; private set; } internal static ConfigEntry MaximumSaveDeferralSeconds { get; private set; } internal 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_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: 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 Enabled = config.Bind("General", "Enabled", true, "Enable bounded world-state observation and optional save smoothing. When false, no Harmony patches or runtime state are created. This never enables deletion or network sync rescheduling."); LogPeriodicSummary = config.Bind("Diagnostics", "LogPeriodicSummary", false, "Write rate-limited aggregate ZDO counts and traffic to the BepInEx log."); SummaryIntervalSeconds = config.Bind("Diagnostics", "SummaryIntervalSeconds", 30f, new ConfigDescription("Seconds between optional aggregate summaries.", (AcceptableValueBase)(object)new AcceptableValueRange(5f, 600f), Array.Empty())); SmoothWorldSaves = config.Bind("Save Smoothing", "Enabled", true, "Coalesce overlapping asynchronous world saves and defer the main-thread PrepareSave capture until a stable frame. Synchronous shutdown saves are never deferred."); SaveFrameBudgetMilliseconds = config.Bind("Save Smoothing", "FrameBudgetMilliseconds", 24f, new ConfigDescription("Prefer to start asynchronous PrepareSave only when the previous unscaled frame was at or below this duration.", (AcceptableValueBase)(object)new AcceptableValueRange(8f, 100f), Array.Empty())); MaximumSaveDeferralSeconds = config.Bind("Save Smoothing", "MaximumDeferralSeconds", 5f, new ConfigDescription("Maximum time an asynchronous save may wait for a stable frame. The save then starts even if frames remain busy.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 30f), Array.Empty())); } } [BepInPlugin("chazman.RunicWorldEngine", "Runic World Engine", "1.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicWorldEngine"; public const string Name = "Runic World Engine"; public const string Version = "1.1.0"; private Harmony _harmony; private float _nextSummaryAt; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; WorldEngineConfig.Bind(((BaseUnityPlugin)this).Config); ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled != null && !enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic World Engine is disabled; no Harmony patches or observatory state were created."); return; } try { _harmony = new Harmony("chazman.RunicWorldEngine"); ObservatoryRuntime.Verify(); _harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic World Engine v1.1.0 ready: bounded aggregate ZDO observability. Unknown data is preserved; compaction and sync changes are off."); } catch (Exception ex) { Shutdown(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic World Engine failed closed; Valheim world handling remains unchanged. " + ex)); } } private void Update() { SaveSmoothingRuntime.Tick(); ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled == null || !enabled.Value) { return; } ConfigEntry logPeriodicSummary = WorldEngineConfig.LogPeriodicSummary; if (logPeriodicSummary != null && logPeriodicSummary.Value) { float unscaledTime = Time.unscaledTime; if (!(unscaledTime < _nextSummaryAt)) { _nextSummaryAt = unscaledTime + Mathf.Clamp(WorldEngineConfig.SummaryIntervalSeconds.Value, 5f, 600f); ZdoObservatorySnapshot current = ObservatoryRuntime.Current; ((BaseUnityPlugin)this).Logger.LogInfo((object)($"World sample #{current.Sequence}: objects={current.TotalObjects}, peers={current.ConnectedPeers}, " + $"created={current.CreatedSincePreviousSample}, destroyed={current.DestroyedSincePreviousSample}, " + $"sent/s={current.SentLastSecond}, received/s={current.ReceivedLastSecond}, " + $"save={current.LastSaveMilliseconds:F1}ms, load={current.LastLoadMilliseconds:F1}ms.")); } } } private void OnDestroy() { Shutdown(); } private void Shutdown() { try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } _harmony = null; ObservatoryRuntime.Reset(); SaveSmoothingRuntime.Reset(); } } } namespace RunicWorldEngine.Integration { [HarmonyPatch(typeof(ZDOMan), "CreateNewZDO", new Type[] { typeof(ZDOID), typeof(Vector3), typeof(int) })] internal static class ZdoCreatedPatch { private static void Postfix(ZDO __result) { ObservatoryRuntime.MarkCreated(__result); } } [HarmonyPatch(typeof(ZDOMan), "HandleDestroyedZDO", new Type[] { typeof(ZDOID) })] internal static class ZdoDestroyedPatch { private static void Prefix(ZDOMan __instance, ZDOID __0, ref bool __state) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) __state = ObservatoryRuntime.Exists(__instance, __0); } private static void Postfix(ZDOMan __instance, ZDOID __0, bool __state) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) ObservatoryRuntime.MarkDestroyed(__state, __instance, __0); } } [HarmonyPatch(typeof(ZDOMan), "UpdateStats", new Type[] { typeof(float) })] internal static class ZdoStatsPatch { private static void Postfix(ZDOMan __instance) { ObservatoryRuntime.Capture(__instance); } } [HarmonyPatch(typeof(ZDOMan), "SaveAsync", new Type[] { typeof(BinaryWriter) })] internal static class ZdoSaveTimingPatch { private static void Prefix(ref long __state) { __state = ObservatoryRuntime.BeginTimedOperation(); } private static Exception Finalizer(long __state, Exception __exception) { ObservatoryRuntime.EndSave(__state); return __exception; } } [HarmonyPatch(typeof(ZDOMan), "Load", new Type[] { typeof(BinaryReader), typeof(int) })] internal static class ZdoLoadTimingPatch { private static void Prefix(ref long __state) { __state = ObservatoryRuntime.BeginTimedOperation(); } private static Exception Finalizer(long __state, Exception __exception) { ObservatoryRuntime.EndLoad(__state); return __exception; } } internal static class ObservatoryRuntime { private static readonly FieldInfo ObjectsField = AccessTools.Field(typeof(ZDOMan), "m_objectsByID"); private static readonly FieldInfo PeersField = AccessTools.Field(typeof(ZDOMan), "m_peers"); private static readonly FieldInfo SentField = AccessTools.Field(typeof(ZDOMan), "m_zdosSentLastSec"); private static readonly FieldInfo ReceivedField = AccessTools.Field(typeof(ZDOMan), "m_zdosRecvLastSec"); private static readonly ObservatoryCounter Counter = new ObservatoryCounter(); private static long _nextCaptureTimestamp; private static bool _verified; internal static ZdoObservatorySnapshot Current => Counter.Current; internal static void Verify() { bool flag = PeersField != null && !PeersField.IsStatic && PeersField.FieldType.IsGenericType && PeersField.FieldType.GetGenericTypeDefinition() == typeof(List<>) && PeersField.FieldType.GetGenericArguments().Length == 1 && PeersField.FieldType.GetGenericArguments()[0].DeclaringType == typeof(ZDOMan) && PeersField.FieldType.GetGenericArguments()[0].Name == "ZDOPeer"; if (ObjectsField == null || ObjectsField.IsStatic || ObjectsField.FieldType != typeof(Dictionary) || !flag || SentField == null || SentField.IsStatic || SentField.FieldType != typeof(int) || ReceivedField == null || ReceivedField.IsStatic || ReceivedField.FieldType != typeof(int)) { throw new MissingMemberException("ZDOMan observatory fields do not match the audited Valheim 0.221.12 contract."); } _verified = true; } internal static void MarkCreated(ZDO created) { if (_verified) { ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled != null && enabled.Value && created != null) { Counter.MarkCreated(); } } } internal static bool Exists(ZDOMan manager, ZDOID id) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (_verified) { ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled != null && enabled.Value && manager != null) { try { return ObjectsField.GetValue(manager) is Dictionary dictionary && dictionary.ContainsKey(id); } catch { return false; } } } return false; } internal static void MarkDestroyed(bool existedBefore, ZDOMan manager, ZDOID id) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (_verified && existedBefore && !Exists(manager, id)) { Counter.MarkDestroyed(); } } internal static void Capture(ZDOMan manager) { if (!_verified || manager == null) { return; } ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled == null || !enabled.Value) { return; } try { long timestamp = Stopwatch.GetTimestamp(); if (timestamp >= _nextCaptureTimestamp) { _nextCaptureTimestamp = ((timestamp > long.MaxValue - Stopwatch.Frequency) ? long.MaxValue : (timestamp + Stopwatch.Frequency)); int totalObjects = ((ObjectsField.GetValue(manager) is Dictionary dictionary) ? dictionary.Count : 0); int connectedPeers = ((PeersField.GetValue(manager) is ICollection collection) ? collection.Count : 0); int sentLastSecond = Convert.ToInt32(SentField.GetValue(manager)); int receivedLastSecond = Convert.ToInt32(ReceivedField.GetValue(manager)); Counter.Capture(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), totalObjects, connectedPeers, sentLastSecond, receivedLastSecond); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("World observatory sample failed closed: " + ex.Message)); } } } internal static long BeginTimedOperation() { if (_verified) { ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled != null && enabled.Value) { return Stopwatch.GetTimestamp(); } } return 0L; } internal static void EndSave(long started) { Counter.RecordSaveDuration(started); } internal static void EndLoad(long started) { Counter.RecordLoadDuration(started); if (ZDOMan.instance != null) { Capture(ZDOMan.instance); } } internal static void Reset() { Counter.Reset(); _nextCaptureTimestamp = 0L; _verified = false; } } internal static class SaveSmoothingRuntime { private static readonly object Gate = new object(); private static readonly FieldInfo SaveThreadField = AccessTools.Field(typeof(ZNet), "m_saveThread"); private static readonly MethodInfo SaveWorldMethod = AccessTools.Method(typeof(ZNet), "SaveWorld", new Type[1] { typeof(bool) }, (Type[])null); private static bool _pending; private static bool _dispatching; private static float _requestedAt; private static float _eligibleAt; internal static bool BeforeSave(ZNet network, bool sync, Thread saveThread) { if (!(WorldEngineConfig.SmoothWorldSaves?.Value ?? true) || sync) { return true; } lock (Gate) { if (_dispatching) { return true; } float unscaledTime = Time.unscaledTime; bool flag = saveThread?.IsAlive ?? false; bool flag2 = Time.unscaledDeltaTime * 1000f > FrameBudget(); if (!flag && !flag2) { return true; } if (!_pending) { _requestedAt = unscaledTime; } _pending = true; _eligibleAt = Math.Max(_eligibleAt, unscaledTime + (flag ? 0.25f : 0.05f)); return false; } } internal static void Tick() { ConfigEntry enabled = WorldEngineConfig.Enabled; if (enabled == null || !enabled.Value) { return; } ConfigEntry smoothWorldSaves = WorldEngineConfig.SmoothWorldSaves; if (smoothWorldSaves != null && !smoothWorldSaves.Value) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } bool flag; lock (Gate) { if (!_pending || _dispatching) { return; } float unscaledTime = Time.unscaledTime; if (unscaledTime < _eligibleAt) { return; } if (SaveThreadField?.GetValue(instance) is Thread { IsAlive: not false }) { _eligibleAt = unscaledTime + 0.25f; return; } bool num = Time.unscaledDeltaTime * 1000f <= FrameBudget(); float num2 = Math.Max(0f, Math.Min(30f, WorldEngineConfig.MaximumSaveDeferralSeconds?.Value ?? 5f)); if (!num && unscaledTime - _requestedAt < num2) { _eligibleAt = unscaledTime + 0.05f; return; } _pending = false; _dispatching = true; flag = true; } if (!flag) { return; } try { if (SaveWorldMethod == null) { throw new MissingMethodException("ZNet.SaveWorld(bool)"); } SaveWorldMethod.Invoke(instance, new object[1] { false }); } finally { lock (Gate) { _dispatching = false; } } } internal static void Reset() { lock (Gate) { _pending = false; _dispatching = false; _requestedAt = 0f; _eligibleAt = 0f; } } private static float FrameBudget() { return Math.Max(8f, Math.Min(100f, WorldEngineConfig.SaveFrameBudgetMilliseconds?.Value ?? 24f)); } } [HarmonyPatch(typeof(ZNet), "SaveWorld", new Type[] { typeof(bool) })] internal static class SmoothWorldSavePatch { [HarmonyPrefix] private static bool Prefix(ZNet __instance, bool sync, Thread ___m_saveThread) { return SaveSmoothingRuntime.BeforeSave(__instance, sync, ___m_saveThread); } } } namespace RunicWorldEngine.Core { internal sealed class ObservatoryCounter { private readonly object _gate = new object(); private int _created; private int _destroyed; private long _sequence; private double _lastSaveMilliseconds; private double _lastLoadMilliseconds; private ZdoObservatorySnapshot _current = ZdoObservatorySnapshot.Empty; internal ZdoObservatorySnapshot Current { get { lock (_gate) { return _current; } } } internal void MarkCreated() { lock (_gate) { _created = SaturatingIncrement(_created); } } internal void MarkDestroyed() { lock (_gate) { _destroyed = SaturatingIncrement(_destroyed); } } internal void RecordSaveDuration(long startedTimestamp) { RecordDuration(startedTimestamp, save: true); } internal void RecordLoadDuration(long startedTimestamp) { RecordDuration(startedTimestamp, save: false); } internal ZdoObservatorySnapshot Capture(long unixMilliseconds, int totalObjects, int connectedPeers, int sentLastSecond, int receivedLastSecond) { lock (_gate) { _sequence = ((_sequence == long.MaxValue) ? long.MaxValue : (_sequence + 1)); _current = new ZdoObservatorySnapshot(_sequence, Math.Max(0L, unixMilliseconds), Math.Max(0, totalObjects), Math.Max(0, connectedPeers), _created, _destroyed, Math.Max(0, sentLastSecond), Math.Max(0, receivedLastSecond), _lastSaveMilliseconds, _lastLoadMilliseconds); _created = 0; _destroyed = 0; return _current; } } internal void Reset() { lock (_gate) { _created = 0; _destroyed = 0; _sequence = 0L; _lastSaveMilliseconds = 0.0; _lastLoadMilliseconds = 0.0; _current = ZdoObservatorySnapshot.Empty; } } private void RecordDuration(long startedTimestamp, bool save) { if (startedTimestamp <= 0) { return; } long num = Stopwatch.GetTimestamp() - startedTimestamp; if (num < 0) { return; } double num2 = (double)num * 1000.0 / (double)Stopwatch.Frequency; lock (_gate) { if (save) { _lastSaveMilliseconds = num2; } else { _lastLoadMilliseconds = num2; } } } private static int SaturatingIncrement(int value) { if (value != int.MaxValue) { return value + 1; } return value; } } } namespace RunicWorldEngine.Contracts { internal sealed class ZdoObservatorySnapshot { internal long Sequence { get; } internal long CapturedUnixMilliseconds { get; } internal int TotalObjects { get; } internal int ConnectedPeers { get; } internal int CreatedSincePreviousSample { get; } internal int DestroyedSincePreviousSample { get; } internal int SentLastSecond { get; } internal int ReceivedLastSecond { get; } internal double LastSaveMilliseconds { get; } internal double LastLoadMilliseconds { get; } internal static ZdoObservatorySnapshot Empty { get; } = new ZdoObservatorySnapshot(0L, 0L, 0, 0, 0, 0, 0, 0, 0.0, 0.0); internal ZdoObservatorySnapshot(long sequence, long capturedUnixMilliseconds, int totalObjects, int connectedPeers, int createdSincePreviousSample, int destroyedSincePreviousSample, int sentLastSecond, int receivedLastSecond, double lastSaveMilliseconds, double lastLoadMilliseconds) { Sequence = Math.Max(0L, sequence); CapturedUnixMilliseconds = Math.Max(0L, capturedUnixMilliseconds); TotalObjects = Math.Max(0, totalObjects); ConnectedPeers = Math.Max(0, connectedPeers); CreatedSincePreviousSample = Math.Max(0, createdSincePreviousSample); DestroyedSincePreviousSample = Math.Max(0, destroyedSincePreviousSample); SentLastSecond = Math.Max(0, sentLastSecond); ReceivedLastSecond = Math.Max(0, receivedLastSecond); LastSaveMilliseconds = Math.Max(0.0, lastSaveMilliseconds); LastLoadMilliseconds = Math.Max(0.0, lastLoadMilliseconds); } } }