Decompiled source of Ragnavik Progress v1.1.0

plugins/RagnavikProgress/RagnavikProgress.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("0.0.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 RagnavikProgress
{
	[BepInPlugin("lostkode.ragnavik.progress", "Ragnavik Progress", "1.1.0")]
	public sealed class RagnavikProgressPlugin : BaseUnityPlugin
	{
		[Serializable]
		private sealed class ProgressReport
		{
			public string server = "";

			public string instance = "";

			public string[] bosses = Array.Empty<string>();

			public PlayerProgress[] players = Array.Empty<PlayerProgress>();

			public BossKill[] bossKills = Array.Empty<BossKill>();

			public int milestoneStep = 10;
		}

		[Serializable]
		private sealed class PlayerProgress
		{
			public string id = "";

			public string name = "";

			public int level;
		}

		[Serializable]
		private sealed class BossKill
		{
			public string id = "";

			public string key = "";

			public string boss = "";

			public string killer = "";

			public string[] participants = Array.Empty<string>();
		}

		private static RagnavikProgressPlugin? _instance;

		private readonly FieldInfo? _globalKeysField = AccessTools.Field(typeof(ZoneSystem), "m_globalKeys");

		private ConfigEntry<bool>? _enabled;

		private ConfigEntry<string>? _endpoint;

		private ConfigEntry<string>? _tokenFile;

		private ConfigEntry<string>? _tokenHeader;

		private ConfigEntry<string>? _serverName;

		private ConfigEntry<int>? _checkSeconds;

		private ConfigEntry<int>? _heartbeatSeconds;

		private ConfigEntry<int>? _milestoneStep;

		private ConfigEntry<float>? _participantRadius;

		private ConfigEntry<bool>? _reportBosses;

		private ConfigEntry<bool>? _reportLevels;

		private float _nextCheck;

		private float _nextHeartbeat;

		private string _lastSignature = "";

		private string _reportSignature = "";

		private Task<bool>? _reportTask;

		private readonly List<BossKill> _pendingBossKills = new List<BossKill>();

		private readonly HashSet<string> _seenBossDeaths = new HashSet<string>(StringComparer.Ordinal);

		private string[] _reportedBossKillIds = Array.Empty<string>();

		private void Awake()
		{
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ea: Expected O, but got Unknown
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_016a: Expected O, but got Unknown
			//IL_0198: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a2: Expected O, but got Unknown
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e0: Expected O, but got Unknown
			_instance = this;
			_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", false, "Enable reporting after a private server endpoint is configured.");
			_endpoint = ((BaseUnityPlugin)this).Config.Bind<string>("Connection", "Endpoint", "", "Private HTTP endpoint that accepts progress reports.");
			_tokenFile = ((BaseUnityPlugin)this).Config.Bind<string>("Connection", "TokenFile", "", "Server-only path containing the authentication token.");
			_tokenHeader = ((BaseUnityPlugin)this).Config.Bind<string>("Connection", "TokenHeader", "X-Progress-Token", "HTTP header used to send the token.");
			_serverName = ((BaseUnityPlugin)this).Config.Bind<string>("Messages", "ServerName", "Valheim", "Server name included in progress reports.");
			_milestoneStep = ((BaseUnityPlugin)this).Config.Bind<int>("Milestones", "EpicMMOLevelStep", 10, new ConfigDescription("Report EpicMMO player levels in this interval.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>()));
			_reportBosses = ((BaseUnityPlugin)this).Config.Bind<bool>("Milestones", "ReportBossDefeats", true, "Report boss defeats, killing player, and nearby participants.");
			_reportLevels = ((BaseUnityPlugin)this).Config.Bind<bool>("Milestones", "ReportEpicMMOLevels", true, "Report connected characters reaching EpicMMO level milestones.");
			_participantRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Milestones", "BossParticipantRadius", 100f, new ConfigDescription("Meters from a defeated boss counted as participating players.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 500f), Array.Empty<object>()));
			_checkSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Timing", "CheckSeconds", 30, new ConfigDescription("Seconds between progress checks.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 600), Array.Empty<object>()));
			_heartbeatSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Timing", "HeartbeatSeconds", 3600, new ConfigDescription("Seconds between unchanged state reports.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(300, 86400), Array.Empty<object>()));
			Harmony.CreateAndPatchAll(typeof(RagnavikProgressPlugin), (string)null);
		}

		private void Update()
		{
			Task<bool> reportTask = _reportTask;
			if (reportTask != null && reportTask.IsCompleted)
			{
				try
				{
					if (_reportTask.GetAwaiter().GetResult())
					{
						_lastSignature = _reportSignature;
						_nextHeartbeat = Time.realtimeSinceStartup + (float)_heartbeatSeconds.Value;
						_pendingBossKills.RemoveAll((BossKill kill) => _reportedBossKillIds.Contains(kill.id));
					}
					else
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)"Progress report returned an unexpected HTTP status.");
						_nextCheck = Time.realtimeSinceStartup + 300f;
					}
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Progress report failed: " + ex.Message));
					_nextCheck = Time.realtimeSinceStartup + 300f;
				}
				_reportTask = null;
			}
			ConfigEntry<bool>? enabled = _enabled;
			if (enabled == null || !enabled.Value || string.IsNullOrWhiteSpace(_endpoint?.Value) || Time.realtimeSinceStartup < _nextCheck)
			{
				return;
			}
			_nextCheck = Time.realtimeSinceStartup + (float)_checkSeconds.Value;
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)ZoneSystem.instance == (Object)null)
			{
				return;
			}
			if (_globalKeysField == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"ZoneSystem.m_globalKeys is unavailable; progress cannot be reported.");
			}
			else
			{
				if (!(_globalKeysField.GetValue(ZoneSystem.instance) is HashSet<string> source))
				{
					return;
				}
				string[] array = (_reportBosses.Value ? source.Where((string key) => key.StartsWith("defeated_", StringComparison.OrdinalIgnoreCase)).OrderBy<string, string>((string key) => key, StringComparer.OrdinalIgnoreCase).ToArray() : Array.Empty<string>());
				PlayerProgress[] array2 = (_reportLevels.Value ? ReadPlayers() : Array.Empty<PlayerProgress>());
				string text = string.Join("|", array) + "#" + string.Join("|", array2.Select((PlayerProgress player) => $"{player.id}:{player.level}")) + "#" + string.Join("|", _pendingBossKills.Select((BossKill kill) => kill.id));
				if (_reportTask == null && (!(text == _lastSignature) || !(Time.realtimeSinceStartup < _nextHeartbeat)))
				{
					string token;
					try
					{
						token = (string.IsNullOrWhiteSpace(_tokenFile?.Value) ? "" : File.ReadAllText(_tokenFile.Value).Trim());
					}
					catch (Exception ex2)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)("Progress token unavailable: " + ex2.Message));
						_nextCheck = Time.realtimeSinceStartup + 600f;
						return;
					}
					string body = JsonUtility.ToJson((object)new ProgressReport
					{
						server = _serverName.Value,
						instance = (Environment.GetEnvironmentVariable("HOSTNAME") ?? ""),
						bosses = array,
						players = array2,
						bossKills = _pendingBossKills.ToArray(),
						milestoneStep = _milestoneStep.Value
					});
					_reportedBossKillIds = _pendingBossKills.Select((BossKill kill) => kill.id).ToArray();
					_reportSignature = text;
					_reportTask = Task.Run(() => SendReport(body, token));
				}
			}
		}

		[HarmonyPatch(typeof(Character), "RPC_Damage")]
		[HarmonyPostfix]
		private static void AfterCharacterDamage(Character __instance, HitData hit)
		{
			if ((Object)(object)_instance == (Object)null)
			{
				return;
			}
			ConfigEntry<bool>? reportBosses = _instance._reportBosses;
			if (reportBosses == null || !reportBosses.Value || (Object)(object)__instance == (Object)null || hit == null || !__instance.IsBoss() || __instance.GetHealth() > 0f)
			{
				return;
			}
			ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
			string text = ((object)Unsafe.As<ZDOID, ZDOID>(ref ((component != null) ? component.GetZDO() : null)?.m_uid)/*cast due to .constrained prefix*/).ToString() ?? $"{((Object)__instance).name}:{Time.frameCount}";
			if (_instance._seenBossDeaths.Add(text))
			{
				string[] participants = (from player in Player.GetAllPlayers()
					where (Object)(object)player != (Object)null && !((Character)player).IsDead() && Vector3.Distance(((Component)player).transform.position, ((Component)__instance).transform.position) <= _instance._participantRadius.Value
					select player.GetPlayerName() into name
					where !string.IsNullOrWhiteSpace(name)
					select name).Distinct<string>(StringComparer.OrdinalIgnoreCase).OrderBy<string, string>((string name) => name, StringComparer.OrdinalIgnoreCase).ToArray();
				Character attacker = hit.GetAttacker();
				Player val = (Player)(object)((attacker is Player) ? attacker : null);
				string killer = ((val != null) ? val.GetPlayerName() : "Unknown Viking");
				_instance._pendingBossKills.Add(new BossKill
				{
					id = text,
					key = (__instance.m_defeatSetGlobalKey ?? ""),
					boss = __instance.GetHoverName(),
					killer = killer,
					participants = participants
				});
				_instance._nextCheck = 0f;
			}
		}

		private PlayerProgress[] ReadPlayers()
		{
			//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_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: 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_004e: 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)
			if (ZDOMan.instance == null)
			{
				return Array.Empty<PlayerProgress>();
			}
			List<PlayerProgress> list = new List<PlayerProgress>();
			foreach (PlayerInfo player in ZNet.instance.GetPlayerList())
			{
				ZDO zDO = ZDOMan.instance.GetZDO(player.m_characterID);
				if (zDO != null)
				{
					PlayerProgress playerProgress = new PlayerProgress();
					ZDOID characterID = player.m_characterID;
					playerProgress.id = ((ZDOID)(ref characterID)).UserID.ToString();
					playerProgress.name = player.m_name ?? "Unknown Viking";
					playerProgress.level = Math.Max(1, zDO.GetInt("EpicMMOSystem_level", 1));
					list.Add(playerProgress);
				}
			}
			return list.OrderBy<PlayerProgress, string>((PlayerProgress player) => player.id, StringComparer.Ordinal).ToArray();
		}

		private bool SendReport(string body, string token)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(body);
			HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(_endpoint.Value);
			httpWebRequest.Method = "POST";
			httpWebRequest.ContentType = "application/json";
			if (!string.IsNullOrWhiteSpace(token))
			{
				httpWebRequest.Headers[_tokenHeader.Value] = token;
			}
			httpWebRequest.Timeout = 3000;
			httpWebRequest.ReadWriteTimeout = 3000;
			httpWebRequest.ContentLength = bytes.Length;
			using (Stream stream = httpWebRequest.GetRequestStream())
			{
				stream.Write(bytes, 0, bytes.Length);
			}
			using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
			return httpWebResponse.StatusCode == HttpStatusCode.NoContent;
		}
	}
}