Decompiled source of UnifiedAutoSaveInterval v1.0.0

plugins\UnifiedAutoSaveInterval\UnifiedAutoSaveInterval.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: AssemblyCompany("Daishi11")]
[assembly: AssemblyTitle("UnifiedAutoSaveInterval")]
[assembly: AssemblyDescription("Configurable combat-aware Valheim autosave interval for hosted worlds and local characters")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyProduct("UnifiedAutoSaveInterval")]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyVersion("1.0.0.0")]
namespace Daishi11.UnifiedAutoSaveInterval;

[BepInPlugin("daishi11.valheim.unifiedautosaveinterval", "Unified Auto Save Interval", "1.0.0")]
public sealed class Plugin : BaseUnityPlugin
{
	public const string PluginGuid = "daishi11.valheim.unifiedautosaveinterval";

	public const string PluginName = "Unified Auto Save Interval";

	public const string PluginVersion = "1.0.0";

	private const float CombatScanIntervalSeconds = 0.25f;

	private static readonly FieldInfo BaseAIField = typeof(Character).GetField("m_baseAI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private ConfigEntry<int> saveIntervalSeconds;

	private ConfigEntry<bool> avoidSavingDuringCombat;

	private ConfigEntry<int> maxCombatDelaySeconds;

	private ConfigEntry<float> combatClearSeconds;

	private ConfigEntry<bool> protectAllPlayersWhenHosting;

	private float combatScanCountdown;

	private float? lastCombatSeenAt;

	private bool lastCombatState;

	private int lastAppliedInterval = -1;

	private void Awake()
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0037: Expected O, but got Unknown
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_0090: Expected O, but got Unknown
		//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Expected O, but got Unknown
		saveIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("General", "SaveIntervalSeconds", 600, new ConfigDescription("Normal Valheim autosave interval in seconds. 600 = 10 minutes, 480 = 8 minutes, 300 = 5 minutes. On an in-game host this controls the hosted world and the host character. On joining clients it controls that client's local character.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(60, 3600), new object[0]));
		avoidSavingDuringCombat = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "AvoidSavingDuringCombat", true, "Temporarily postpone an autosave while relevant players are in combat.");
		maxCombatDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Combat", "MaxCombatDelaySeconds", 120, new ConfigDescription("Maximum extra seconds a combat can postpone an autosave. With a 600 second normal interval and 120 second delay, a save is forced no later than 720 seconds after the previous save.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 600), new object[0]));
		combatClearSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Combat", "CombatClearSeconds", 5f, new ConfigDescription("How many seconds combat must remain clear before the normal autosave interval is restored. This prevents target switching from making combat flicker on and off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), new object[0]));
		protectAllPlayersWhenHosting = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "ProtectAllPlayersWhenHosting", true, "When this Valheim instance hosts a world via Start Server, postpone the world autosave if any connected player is being targeted by a creature. Joining clients only protect their own local character save.");
		saveIntervalSeconds.SettingChanged += OnSettingChanged;
		avoidSavingDuringCombat.SettingChanged += OnSettingChanged;
		maxCombatDelaySeconds.SettingChanged += OnSettingChanged;
		combatClearSeconds.SettingChanged += OnSettingChanged;
		protectAllPlayersWhenHosting.SettingChanged += OnSettingChanged;
		SceneManager.sceneLoaded += OnSceneLoaded;
		ResetCombatState();
		RefreshInterval(forceLog: true);
	}

	private void Update()
	{
		combatScanCountdown -= Time.unscaledDeltaTime;
		if (!(combatScanCountdown > 0f))
		{
			combatScanCountdown = 0.25f;
			RefreshInterval(forceLog: false);
		}
	}

	private void OnDestroy()
	{
		if (saveIntervalSeconds != null)
		{
			saveIntervalSeconds.SettingChanged -= OnSettingChanged;
		}
		if (avoidSavingDuringCombat != null)
		{
			avoidSavingDuringCombat.SettingChanged -= OnSettingChanged;
		}
		if (maxCombatDelaySeconds != null)
		{
			maxCombatDelaySeconds.SettingChanged -= OnSettingChanged;
		}
		if (combatClearSeconds != null)
		{
			combatClearSeconds.SettingChanged -= OnSettingChanged;
		}
		if (protectAllPlayersWhenHosting != null)
		{
			protectAllPlayersWhenHosting.SettingChanged -= OnSettingChanged;
		}
		SceneManager.sceneLoaded -= OnSceneLoaded;
	}

	private void OnSettingChanged(object sender, EventArgs args)
	{
		lastAppliedInterval = -1;
		RefreshInterval(forceLog: true);
	}

	private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
	{
		ResetCombatState();
		lastAppliedInterval = -1;
		RefreshInterval(forceLog: true);
	}

	private void ResetCombatState()
	{
		combatScanCountdown = 0f;
		lastCombatSeenAt = null;
		lastCombatState = false;
	}

	private void RefreshInterval(bool forceLog)
	{
		bool flag = avoidSavingDuringCombat.Value && maxCombatDelaySeconds.Value > 0 && IsRelevantCombatActive();
		int value = saveIntervalSeconds.Value;
		int num = value;
		if (flag)
		{
			num += maxCombatDelaySeconds.Value;
		}
		bool flag2 = lastAppliedInterval != num || Math.Abs(Game.m_saveInterval - (float)num) > 0.01f;
		if (flag2)
		{
			Game.m_saveInterval = num;
			lastAppliedInterval = num;
		}
		if (flag != lastCombatState)
		{
			lastCombatState = flag;
			if (flag)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Combat detected. Autosave may be postponed by up to " + maxCombatDelaySeconds.Value + " seconds; effective maximum interval is " + num + " seconds."));
			}
			else
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Combat clear. Normal autosave interval restored to " + value + " seconds."));
			}
		}
		else if (forceLog || flag2)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Valheim autosave interval set to " + num + " seconds (" + ((double)num / 60.0).ToString("0.##") + " minutes)" + (flag ? " while combat protection is active." : ".")));
		}
	}

	private bool IsRelevantCombatActive()
	{
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null)
		{
			lastCombatSeenAt = null;
			return false;
		}
		bool flag = protectAllPlayersWhenHosting.Value && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();
		List<Player> list = null;
		if (flag)
		{
			list = Player.GetAllPlayers();
		}
		List<Character> allCharacters = Character.GetAllCharacters();
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (allCharacters != null)
		{
			for (int i = 0; i < allCharacters.Count; i++)
			{
				Character val = allCharacters[i];
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				BaseAI baseAI = GetBaseAI(val);
				if ((Object)(object)baseAI == (Object)null)
				{
					continue;
				}
				Character targetCreature = baseAI.GetTargetCreature();
				if ((Object)(object)targetCreature == (Object)null)
				{
					continue;
				}
				bool flag2 = false;
				if (flag && list != null)
				{
					for (int j = 0; j < list.Count; j++)
					{
						Player val2 = list[j];
						if ((Object)(object)val2 != (Object)null && object.ReferenceEquals(targetCreature, val2))
						{
							flag2 = true;
							break;
						}
					}
				}
				else
				{
					flag2 = object.ReferenceEquals(targetCreature, localPlayer);
				}
				if (flag2)
				{
					lastCombatSeenAt = realtimeSinceStartup;
					return true;
				}
			}
		}
		if (!lastCombatSeenAt.HasValue)
		{
			return false;
		}
		return realtimeSinceStartup - lastCombatSeenAt.Value < combatClearSeconds.Value;
	}

	private static BaseAI GetBaseAI(Character character)
	{
		if (BaseAIField == null)
		{
			return null;
		}
		try
		{
			object? value = BaseAIField.GetValue(character);
			return (BaseAI)((value is BaseAI) ? value : null);
		}
		catch
		{
			return null;
		}
	}
}