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 ValheimOnlineTracker v2.0.0
ValheimStatsToDiscord.dll
Decompiled a year agousing 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."); } } }