Decompiled source of ValheimOnlineTracker v2.0.0

ValheimStatsToDiscord.dll

Decompiled 3 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
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("ValheimStatsToDiscord")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("ValheimStatsToDiscord")]
[assembly: AssemblyTitle("ValheimStatsToDiscord")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 ValheimStatsToDiscord
{
	[BepInPlugin("com.gamemaster.valheimonlinetracker", "Valheim Online Tracker", "1.5.1")]
	public class ValheimOnlineTrackerPlugin : BaseUnityPlugin
	{
		public class PlayerStats
		{
			public int Today;

			public int Week;

			public int Total;
		}

		public class LoginData
		{
			public List<string> Today { get; set; }

			public List<string> Week { get; set; }

			public List<string> AllTime { get; set; }
		}

		public class SaveData
		{
			public Dictionary<string, PlayerStats> TimeTracked { get; set; }

			public LoginData LoginTracking { get; set; }

			public Dictionary<string, HashSet<string>> MilestonesHit { get; set; }
		}

		public static ManualLogSource Log;

		private static readonly HttpClient http = new HttpClient();

		private float postTimer;

		private float trackTimer;

		private float loginStatsTimer;

		private ConfigEntry<string> WebhookURL;

		private ConfigEntry<string> StatsWebhookURL;

		private ConfigEntry<int> PostInterval;

		private ConfigEntry<string> StartupMessages;

		private ConfigEntry<string> ShutdownMessages;

		private ConfigEntry<string> OnlineTitle;

		private ConfigEntry<string> NoPlayersMessage;

		private ConfigEntry<string> BotName;

		private ConfigEntry<bool> EnableLoginLogout;

		private ConfigEntry<string> LoginMessages;

		private ConfigEntry<string> LogoutMessages;

		private ConfigEntry<bool> EnableLoginStatsPost;

		private ConfigEntry<int> LoginStatsInterval;

		private ConfigEntry<string> LoginStatsMessages;

		private ConfigEntry<string> LoginMilestoneMessage;

		private ConfigEntry<string> TimeMilestoneMessage;

		private string statsPath;

		private Dictionary<string, PlayerStats> playerStats = new Dictionary<string, PlayerStats>();

		private Dictionary<string, HashSet<string>> milestonesHit = new Dictionary<string, HashSet<string>>();

		private HashSet<string> currentPlayers = new HashSet<string>();

		private HashSet<string> loginsToday = new HashSet<string>();

		private HashSet<string> loginsWeek = new HashSet<string>();

		private HashSet<string> loginsAllTime = new HashSet<string>();

		public void Awake()
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			string text = Path.Combine(Paths.ConfigPath, "OnlineTracker");
			Directory.CreateDirectory(text);
			ConfigFile config = new ConfigFile(Path.Combine(text, "ValheimOnlineTracker.cfg"), true);
			statsPath = Path.Combine(text, "Viking_Stats.json");
			SetupConfig(config);
			LoadStats();
			Task.Run(async delegate
			{
				if (!string.IsNullOrWhiteSpace(WebhookURL.Value))
				{
					await SendDiscord(PickRandom(StartupMessages.Value), useStatsWebhook: false);
				}
			});
		}

		public void Update()
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			float deltaTime = Time.deltaTime;
			postTimer += deltaTime;
			trackTimer += deltaTime;
			loginStatsTimer += deltaTime;
			if (trackTimer >= 60f)
			{
				TrackPlayerTime();
				trackTimer = 0f;
			}
			CheckPlayerLogins();
			if (postTimer >= (float)PostInterval.Value)
			{
				postTimer = 0f;
				Task.Run(() => PostOnlinePlayers());
			}
			if (EnableLoginStatsPost.Value && loginStatsTimer >= (float)LoginStatsInterval.Value)
			{
				loginStatsTimer = 0f;
				Task.Run((Func<Task?>)PostLoginStats);
			}
		}

		public void OnDestroy()
		{
			PlayerTrackerSave();
			Task.Run(async delegate
			{
				if (!string.IsNullOrWhiteSpace(ShutdownMessages.Value))
				{
					await SendDiscord(PickRandom(ShutdownMessages.Value), useStatsWebhook: false);
				}
			});
		}

		private async Task PostLoginStats()
		{
			string message = LoginStatsMessages.Value.Replace("{today}", loginsToday.Count.ToString()).Replace("{week}", loginsWeek.Count.ToString()).Replace("{alltime}", loginsAllTime.Count.ToString());
			await SendDiscord(message, useStatsWebhook: true);
		}

		private async Task PostOnlinePlayers()
		{
			if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrWhiteSpace(WebhookURL.Value))
			{
				return;
			}
			List<string> list = (from p in ZNet.instance.GetConnectedPeers()
				where !string.IsNullOrEmpty(p.m_playerName)
				select p.m_playerName).Distinct().ToList();
			if (list.Count == 0)
			{
				await SendDiscord(NoPlayersMessage.Value, useStatsWebhook: false);
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine($"{OnlineTitle.Value} ({list.Count})");
			foreach (string item in list)
			{
				if (!playerStats.TryGetValue(item, out var value))
				{
					value = new PlayerStats();
				}
				stringBuilder.AppendLine("⏱ " + item + " — Today: " + FormatTime(value.Today) + " | Week: " + FormatTime(value.Week) + " | Total: " + FormatTime(value.Total));
			}
			await SendDiscord(stringBuilder.ToString(), useStatsWebhook: true);
		}

		private string FormatTime(int minutes)
		{
			int num = minutes / 60;
			int num2 = minutes % 60;
			if (num <= 0)
			{
				return $"{num2}m";
			}
			return $"{num}h {num2}m";
		}

		private string PickRandom(string input)
		{
			string[] array = input.Split(new char[1] { '|' });
			return array[Random.Range(0, array.Length)].Trim();
		}

		private void CheckPlayerLogins()
		{
			if (!EnableLoginLogout.Value)
			{
				return;
			}
			HashSet<string> hashSet = (from p in ZNet.instance.GetConnectedPeers()
				where !string.IsNullOrEmpty(p.m_playerName)
				select p.m_playerName).ToHashSet();
			List<string> list = hashSet.Except(currentPlayers).ToList();
			List<string> list2 = currentPlayers.Except(hashSet).ToList();
			foreach (string item in list)
			{
				string msg2 = PickRandom(LoginMessages.Value).Replace("{player}", item);
				Task.Run(() => SendDiscord(msg2, useStatsWebhook: true));
				loginsToday.Add(item);
				loginsWeek.Add(item);
				loginsAllTime.Add(item);
				CheckLoginMilestone(item);
			}
			foreach (string item2 in list2)
			{
				string msg = PickRandom(LogoutMessages.Value).Replace("{player}", item2);
				Task.Run(() => SendDiscord(msg, useStatsWebhook: true));
			}
			currentPlayers = hashSet;
		}

		private void TrackPlayerTime()
		{
			foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers())
			{
				string playerName = connectedPeer.m_playerName;
				if (!string.IsNullOrWhiteSpace(playerName))
				{
					if (!playerStats.ContainsKey(playerName))
					{
						playerStats[playerName] = new PlayerStats();
					}
					playerStats[playerName].Today++;
					playerStats[playerName].Week++;
					playerStats[playerName].Total++;
					CheckTimeMilestone(playerName, playerStats[playerName].Total);
				}
			}
			PlayerTrackerSave();
		}

		private void CheckLoginMilestone(string name)
		{
			if (!milestonesHit.ContainsKey(name))
			{
				milestonesHit[name] = new HashSet<string>();
			}
			if (loginsAllTime.Count((string n) => n == name) >= 100 && !milestonesHit[name].Contains("100logins"))
			{
				milestonesHit[name].Add("100logins");
				string msg = LoginMilestoneMessage.Value.Replace("{player}", name);
				Task.Run(() => SendDiscord(msg, useStatsWebhook: true));
			}
		}

		private void CheckTimeMilestone(string name, int totalMinutes)
		{
			if (!milestonesHit.ContainsKey(name))
			{
				milestonesHit[name] = new HashSet<string>();
			}
			if (totalMinutes >= 3000 && !milestonesHit[name].Contains("50hours"))
			{
				milestonesHit[name].Add("50hours");
				string msg = TimeMilestoneMessage.Value.Replace("{player}", name);
				Task.Run(() => SendDiscord(msg, useStatsWebhook: true));
			}
		}

		private async Task SendDiscord(string message, bool useStatsWebhook)
		{
			string text = ((useStatsWebhook && !string.IsNullOrWhiteSpace(StatsWebhookURL.Value)) ? StatsWebhookURL.Value : WebhookURL.Value);
			if (string.IsNullOrWhiteSpace(text))
			{
				return;
			}
			string content = JsonConvert.SerializeObject((object)new
			{
				username = BotName.Value,
				content = message
			});
			try
			{
				HttpResponseMessage httpResponseMessage = await http.PostAsync(text, new StringContent(content, Encoding.UTF8, "application/json"));
				if (!httpResponseMessage.IsSuccessStatusCode)
				{
					ManualLogSource log = Log;
					string text2 = httpResponseMessage.StatusCode.ToString();
					log.LogWarning((object)("[OnlineTracker] Discord response: " + text2 + " - " + await httpResponseMessage.Content.ReadAsStringAsync()));
				}
			}
			catch (Exception ex)
			{
				Log.LogError((object)("[OnlineTracker] Failed to send Discord message: " + ex.Message));
			}
		}

		private void PlayerTrackerSave()
		{
			try
			{
				SaveData saveData = new SaveData
				{
					TimeTracked = playerStats,
					LoginTracking = new LoginData
					{
						Today = loginsToday.ToList(),
						Week = loginsWeek.ToList(),
						AllTime = loginsAllTime.ToList()
					},
					MilestonesHit = milestonesHit
				};
				File.WriteAllText(statsPath, JsonConvert.SerializeObject((object)saveData, (Formatting)1));
			}
			catch (Exception ex)
			{
				Log.LogError((object)("Failed to save stats: " + ex.Message));
			}
		}

		private void LoadStats()
		{
			try
			{
				if (File.Exists(statsPath))
				{
					SaveData saveData = JsonConvert.DeserializeObject<SaveData>(File.ReadAllText(statsPath));
					playerStats = saveData.TimeTracked ?? new Dictionary<string, PlayerStats>();
					loginsToday = new HashSet<string>(saveData.LoginTracking?.Today ?? new List<string>());
					loginsWeek = new HashSet<string>(saveData.LoginTracking?.Week ?? new List<string>());
					loginsAllTime = new HashSet<string>(saveData.LoginTracking?.AllTime ?? new List<string>());
					milestonesHit = saveData.MilestonesHit ?? new Dictionary<string, HashSet<string>>();
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Could not load stats: " + ex.Message));
			}
		}

		private void SetupConfig(ConfigFile config)
		{
			WebhookURL = config.Bind<string>("General", "WebhookURL", "", "Main Discord Webhook URL.");
			StatsWebhookURL = config.Bind<string>("General", "StatsWebhookURL", "", "Optional secondary webhook for stats / milestones.");
			PostInterval = config.Bind<int>("General", "PostInterval", 600, "Post interval in seconds. Default = 10 min.");
			BotName = config.Bind<string>("General", "BotName", "Online Tracker", "Name shown in Discord.");
			StartupMessages = config.Bind<string>("Messages", "StartupMessages", "\ud83d\udfe2 Server is now starting!|\ud83d\udfe2 The Lands of Dredd awakens!", "Startup messages.");
			ShutdownMessages = config.Bind<string>("Messages", "ShutdownMessages", "\ud83d\udd34 Server shutting down...|\ud83d\udd34 The Lands of Dredd is going to sleep.", "Shutdown messages.");
			OnlineTitle = config.Bind<string>("Messages", "OnlineTitle", "\ud83d\udfe2 Vikings Online", "Header for online player list.");
			NoPlayersMessage = config.Bind<string>("Messages", "NoPlayersMessage", "\ud83d\udfe2 Server is LIVE – no players online at the moment.", "Message when no one is online.");
			EnableLoginLogout = config.Bind<bool>("Logins", "EnableLoginLogout", true, "Enable login/logout announcements.");
			LoginMessages = config.Bind<string>("Logins", "LoginMessages", "\ud83d\udfe2 {player} has joined the realm!|\ud83d\udfe2 Welcome back, {player}!", "Pipe-separated login messages.");
			LogoutMessages = config.Bind<string>("Logins", "LogoutMessages", "\ud83d\udd34 {player} has left the world.|\ud83d\udd34 Farewell, {player}.", "Pipe-separated logout messages.");
			EnableLoginStatsPost = config.Bind<bool>("LoginStats", "EnableLoginStatsPost", true, "Enable posting login stats.");
			LoginStatsInterval = config.Bind<int>("LoginStats", "LoginStatsInterval", 1800, "How often to post login stats (seconds). Default = 30 min.");
			LoginStatsMessages = config.Bind<string>("LoginStats", "LoginStatsMessages", "\ud83d\udcca Login Stats Today: {today} This Week: {week} All Time: {alltime}", "Use placeholders {today}, {week}, {alltime}.");
			LoginMilestoneMessage = config.Bind<string>("Milestones", "LoginMilestoneMessage", "\ud83c\udfc5 {player} has logged in 100 times! What a legend!", "Message when 100 logins is reached.");
			TimeMilestoneMessage = config.Bind<string>("Milestones", "TimeMilestoneMessage", "⏳ {player} has reached 50 hours of gameplay!", "Message when 3000 minutes is reached.");
		}
	}
}