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.");
}
}
}