Иконка ресурса

FREE PlaytimeTracker : 1.1.1 1.1.1

Нет прав для скачивания
C#:
/*
 * Copyright (C) 2024 Game4Freak.io
 * This mod is provided under the Game4Freak EULA.
 * Full legal terms can be found at https://game4freak.io/eula/
 */

using Network;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Playtime Tracker", "VisEntities", "1.1.1")]
    [Description("Provides an API to track player playtime and AFK time for other plugins.")]
    public class PlaytimeTracker : RustPlugin
    {
        #region Fields

        private static PlaytimeTracker _plugin;
        private static Configuration _config;
        private StoredData _storedData;
        private readonly Dictionary<ulong, AfkTracker> _afkTrackers = new Dictionary<ulong, AfkTracker>();
        private readonly Dictionary<ulong, SessionSnapshot> _sessionSnapshots = new Dictionary<ulong, SessionSnapshot>();

        #endregion Fields

        #region Configuration

        private class Configuration
        {
            [JsonProperty("Version")]
            public string Version { get; set; }

            [JsonProperty("Update Interval Seconds")]
            public int UpdateIntervalSeconds { get; set; }

            [JsonProperty("Enable Afk Tracking")]
            public bool EnableAfkTracking { get; set; }

            [JsonProperty("Idle Timeout Seconds")]
            public int IdleTimeoutSeconds { get; set; }

            [JsonProperty("Data Autosave Interval Seconds")]
            public int DataAutosaveIntervalSeconds { get; set; }

            [JsonProperty("Reset Data On New Save")]
            public bool ResetDataOnNewSave { get; set; }

            [JsonProperty("Top Leaderboard Entries")]
            public int TopLeaderboardEntries { get; set; }

            [JsonProperty("Do Not Track Players")]
            public List<ulong> DoNotTrackPlayers { get; set; }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            _config = Config.ReadObject<Configuration>();

            if (string.Compare(_config.Version, Version.ToString()) < 0)
                UpdateConfig();

            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            _config = GetDefaultConfig();
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        private void UpdateConfig()
        {
            PrintWarning("Config changes detected! Updating...");

            Configuration defaultConfig = GetDefaultConfig();

            if (string.Compare(_config.Version, "1.0.0") < 0)
                _config = defaultConfig;

            PrintWarning("Config update complete! Updated from version " + _config.Version + " to " + Version.ToString());
            _config.Version = Version.ToString();
        }

        private Configuration GetDefaultConfig()
        {
            return new Configuration
            {
                Version = Version.ToString(),
                UpdateIntervalSeconds = 60,
                EnableAfkTracking = true,
                IdleTimeoutSeconds = 300,
                DataAutosaveIntervalSeconds = 300,
                ResetDataOnNewSave = false,
                TopLeaderboardEntries = 10,
                DoNotTrackPlayers = new List<ulong>()
            };
        }

        #endregion Configuration

        #region Stored Data

        public class StoredData
        {
            [JsonProperty("Players")]
            public Dictionary<ulong, PlayerData> Players { get; set; } = new Dictionary<ulong, PlayerData>();
        }

        public class PlayerData
        {
            [JsonProperty("Name")]
            public string Name { get; set; }

            [JsonProperty("Playtime Seconds")]
            public double PlaytimeSeconds { get; set; }

            [JsonProperty("Afk Seconds")]
            public double AfkSeconds { get; set; }

            [JsonProperty("Last Logout Epoch")]
            public double LastLogoutEpoch { get; set; }

            [JsonProperty("Last Session Active Seconds")]
            public double LastSessionActiveSeconds { get; set; }
        }

        private PlayerData GetOrCreatePlayerData(BasePlayer player)
        {
            if (!_storedData.Players.TryGetValue(player.userID, out var playerData))
            {
                playerData = new PlayerData
                {
                    Name = player.displayName,
                    PlaytimeSeconds = 0,
                    AfkSeconds = 0
                };
                _storedData.Players[player.userID] = playerData;
            }
            else
                playerData.Name = player.displayName;

            return playerData;
        }

        private void SaveLoop() => timer.In(_config.DataAutosaveIntervalSeconds, () =>
        {
            DataFileUtil.Save(DataFileUtil.GetFilePath(), _storedData);
            SaveLoop();
        });

        #endregion Stored Data

        #region Oxide Hooks

        private void Init()
        {
            _plugin = this;
            PermissionUtil.RegisterPermissions();
            _storedData = DataFileUtil.LoadOrCreate<StoredData>(DataFileUtil.GetFilePath());
        }

        private void OnServerInitialized(bool isStartup)
        {
            timer.Every(_config.UpdateIntervalSeconds, UpdatePlaytime);
            SaveLoop();
        }

        private void Unload()
        {
            DataFileUtil.Save(DataFileUtil.GetFilePath(), _storedData);
            _plugin = null;
            _config = null;
        }

        private void OnNewSave(string filename)
        {
            if (!_config.ResetDataOnNewSave)
                return;

            DataFileUtil.Delete(DataFileUtil.GetFilePath());
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (IsPlayerExcluded(player.userID))
                return;

            _afkTrackers[player.userID] = new AfkTracker
            {
                LastPosition = player.transform.position,
                LastMoveTime = Time.realtimeSinceStartup
            };
            GetOrCreatePlayerData(player);

            _sessionSnapshots[player.userID] = new SessionSnapshot
            {
                PlayStart = API_GetPlaytimeSeconds(player.userID),
                AfkStart = API_GetAfkSeconds(player.userID)
            };
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            SavePlayerTimeOnDisconnect(player);

            if (_sessionSnapshots.TryGetValue(player.userID, out SessionSnapshot snap))
            {
                double totalPlay = API_GetPlaytimeSeconds(player.userID);
                double totalAfk = API_GetAfkSeconds(player.userID);
                double active = (totalPlay - snap.PlayStart) - (totalAfk - snap.AfkStart);
                if (active < 0)
                    active = 0;

                PlayerData pd = GetOrCreatePlayerData(player);
                pd.LastSessionActiveSeconds = active;
                pd.LastLogoutEpoch = TimeEx.currentTimestamp;

                ExposedHooks.OnPlayerSessionEnded(player, active, pd.LastLogoutEpoch);
            }
            _sessionSnapshots.Remove(player.userID);

            _afkTrackers.Remove(player.userID);
        }

        #endregion Oxide Hooks

        #region Core

        private void UpdatePlaytime()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected)
                    continue;

                if (IsPlayerExcluded(player.userID))
                    continue;

                if (!_afkTrackers.TryGetValue(player.userID, out var inactivityTracker))
                {
                    inactivityTracker = new AfkTracker
                    {
                        LastPosition = player.transform.position,
                        LastMoveTime = Time.realtimeSinceStartup
                    };
                    _afkTrackers[player.userID] = inactivityTracker;
                }

                var playerData = GetOrCreatePlayerData(player);
                bool moved = Vector3.Distance(player.transform.position, inactivityTracker.LastPosition) > 0.1f;

                if (moved)
                {
                    playerData.PlaytimeSeconds += _config.UpdateIntervalSeconds;
                    inactivityTracker.LastPosition = player.transform.position;
                    inactivityTracker.LastMoveTime = Time.realtimeSinceStartup;
                }
                else
                {
                    if (_config.EnableAfkTracking)
                    {
                        float idleSeconds = Time.realtimeSinceStartup - inactivityTracker.LastMoveTime;
                        if (idleSeconds >= _config.IdleTimeoutSeconds)
                            playerData.AfkSeconds += _config.UpdateIntervalSeconds;
                        else
                            playerData.PlaytimeSeconds += _config.UpdateIntervalSeconds;
                    }
                    else
                    {
                        playerData.PlaytimeSeconds += _config.UpdateIntervalSeconds;
                    }
                }

                bool isAfkNow = _config.EnableAfkTracking &&
                    !moved &&
                    (Time.realtimeSinceStartup - inactivityTracker.LastMoveTime) >= _config.IdleTimeoutSeconds;
                ExposedHooks.OnPlaytimeUpdate(player, playerData.PlaytimeSeconds, playerData.AfkSeconds, isAfkNow);
            }
        }

        private void SavePlayerTimeOnDisconnect(BasePlayer player)
        {
            if (!_afkTrackers.TryGetValue(player.userID, out AfkTracker afkTracker))
                return;

            PlayerData data = GetOrCreatePlayerData(player);
            float idleSeconds = Time.realtimeSinceStartup - afkTracker.LastMoveTime;

            if (_config.EnableAfkTracking && idleSeconds >= _config.IdleTimeoutSeconds)
                data.AfkSeconds += idleSeconds;
            else
                data.PlaytimeSeconds += idleSeconds;
        }

        #endregion Core

        #region Trackers

        private class AfkTracker
        {
            public Vector3 LastPosition;
            public float LastMoveTime;
        }

        private class SessionSnapshot
        {
            public double PlayStart;
            public double AfkStart;
        }

        #endregion Trackers

        #region API

        [HookMethod(nameof(API_GetPlaytimeSeconds))]
        public double API_GetPlaytimeSeconds(ulong playerId)
        {
            PlayerData playerData;
            if (_storedData.Players.TryGetValue(playerId, out playerData))
            {
                return playerData.PlaytimeSeconds;
            }

            return 0d;
        }

        [HookMethod(nameof(API_GetAfkSeconds))]
        public double API_GetAfkSeconds(ulong playerId)
        {
            PlayerData playerData;
            if (_storedData.Players.TryGetValue(playerId, out playerData))
            {
                return playerData.AfkSeconds;
            }

            return 0d;
        }

        [HookMethod(nameof(API_GetFormattedPlaytime))]
        public string API_GetFormattedPlaytime(ulong playerId)
        {
            double playtimeSeconds = API_GetPlaytimeSeconds(playerId);
            return FormatTime(playtimeSeconds);
        }

        [HookMethod(nameof(API_GetTopPlaytimes))]
        public List<Dictionary<string, object>> API_GetTopPlaytimes(int count = 10)
        {
            IEnumerable<KeyValuePair<ulong, PlayerData>> orderedPlayers =
                _storedData.Players
                           .OrderByDescending(pair => pair.Value.PlaytimeSeconds)
                           .Take(count);

            List<Dictionary<string, object>> topList = new List<Dictionary<string, object>>();

            foreach (KeyValuePair<ulong, PlayerData> pair in orderedPlayers)
            {
                Dictionary<string, object> entry = new Dictionary<string, object>
                {
                    ["PlayerId"] = pair.Key,
                    ["PlaytimeSeconds"] = pair.Value.PlaytimeSeconds
                };

                topList.Add(entry);
            }

            return topList;
        }

        [HookMethod(nameof(API_GetLastSessionActiveSeconds))]
        public double API_GetLastSessionActiveSeconds(ulong playerId)
        {
            PlayerData data;
            if (_storedData.Players.TryGetValue(playerId, out data))
            {
                return data.LastSessionActiveSeconds;
            }

            return 0d;
        }

        [HookMethod(nameof(API_GetLastLogoutEpoch))]
        public double API_GetLastLogoutEpoch(ulong playerId)
        {
            PlayerData data;
            if (_storedData.Players.TryGetValue(playerId, out data))
            {
                return data.LastLogoutEpoch;
            }

            return 0d;
        }

        #endregion API

        #region Exposed Hooks

        private static class ExposedHooks
        {
            public static void OnPlaytimeUpdate(BasePlayer player, double playtime, double afkTime, bool isAfkNow)
            {
                var timeData = new Dictionary<string, object>
                {
                    ["PlaytimeSeconds"] = playtime,
                    ["AfkSeconds"] = afkTime,
                    ["IsCurrentlyAfk"] = isAfkNow
                };

                Interface.CallHook("OnPlaytimeUpdate", player, timeData);
            }

            public static void OnPlayerSessionEnded(BasePlayer player, double sessionActiveSeconds, double logoutEpoch)
            {
                Interface.CallHook("OnPlayerSessionEnded", player, sessionActiveSeconds, logoutEpoch);
            }
        }

        #endregion Exposed Hooks

        #region Helper Functions

        private static string FormatTime(double seconds)
        {
            var t = TimeSpan.FromSeconds(seconds);
            int hours = (int)t.TotalHours;
            return $"{hours:00}h:{t.Minutes:00}m:{t.Seconds:00}s";
        }

        private BasePlayer FindPlayerByPartialNameOrId(string nameOrId)
        {
            if (string.IsNullOrEmpty(nameOrId))
                return null;

            if (ulong.TryParse(nameOrId, out ulong id))
            {
                BasePlayer found = BasePlayer.activePlayerList.FirstOrDefault(x => x.userID == id);
                if (found != null)
                    return found;
            }

            nameOrId = nameOrId.ToLower();
            List<BasePlayer> matches = BasePlayer.activePlayerList.Where(x => x.displayName.ToLower().Contains(nameOrId)).ToList();
            if (matches.Count == 1)
                return matches[0];

            return null;
        }

        private bool IsPlayerExcluded(ulong playerId)
        {
            return _config.DoNotTrackPlayers != null &&
                   _config.DoNotTrackPlayers.Contains(playerId);
        }

        #endregion Helper Functions

        #region Helper Classes

        public static class DataFileUtil
        {
            private const string FOLDER = "";

            public static void EnsureFolderCreated()
            {
                string path = Path.Combine(Interface.Oxide.DataDirectory, FOLDER);

                if (!Directory.Exists(path))
                    Directory.CreateDirectory(path);
            }

            public static string GetFilePath(string filename = null)
            {
                if (filename == null)
                    filename = _plugin.Name;

                return Path.Combine(FOLDER, filename);
            }

            public static string[] GetAllFilePaths()
            {
                string[] filePaths = Interface.Oxide.DataFileSystem.GetFiles(FOLDER);
                for (int i = 0; i < filePaths.Length; i++)
                {
                    filePaths[i] = filePaths[i].Substring(0, filePaths[i].Length - 5);
                }

                return filePaths;
            }

            public static bool Exists(string filePath)
            {
                return Interface.Oxide.DataFileSystem.ExistsDatafile(filePath);
            }

            public static T Load<T>(string filePath) where T : class, new()
            {
                T data = Interface.Oxide.DataFileSystem.ReadObject<T>(filePath);
                if (data == null)
                    data = new T();

                return data;
            }

            public static T LoadIfExists<T>(string filePath) where T : class, new()
            {
                if (Exists(filePath))
                    return Load<T>(filePath);
                else
                    return null;
            }

            public static T LoadOrCreate<T>(string filePath) where T : class, new()
            {
                T data = LoadIfExists<T>(filePath);
                if (data == null)
                    data = new T();

                return data;
            }

            public static void Save<T>(string filePath, T data)
            {
                Interface.Oxide.DataFileSystem.WriteObject<T>(filePath, data);
            }

            public static void Delete(string filePath)
            {
                Interface.Oxide.DataFileSystem.DeleteDataFile(filePath);
            }
        }

        #endregion Helper Classes

        #region Permissions

        private static class PermissionUtil
        {
            public const string USE = "playtimetracker.use";
            private static readonly List<string> _permissions = new List<string>
            {
                USE,
            };

            public static void RegisterPermissions()
            {
                foreach (var permission in _permissions)
                {
                    _plugin.permission.RegisterPermission(permission, _plugin);
                }
            }

            public static bool HasPermission(BasePlayer player, string permissionName)
            {
                return _plugin.permission.UserHasPermission(player.UserIDString, permissionName);
            }
        }

        #endregion Permissions

        #region Commands

        [ChatCommand("playtime")]
        private void cmdPlaytime(BasePlayer player, string command, string[] args)
        {
            if (player == null || !PermissionUtil.HasPermission(player, PermissionUtil.USE))
            {
                MessagePlayer(player, Lang.Error_NoPermission);
                return;
            }

            if (args.Length == 0)
            {
                PlayerData data = GetOrCreatePlayerData(player);
                MessagePlayer(player, Lang.Info_SelfTime, FormatTime(data.PlaytimeSeconds), FormatTime(data.AfkSeconds));
                MessagePlayer(player, Lang.Help_General);
                return;
            }

            if (args[0].Equals("top", StringComparison.OrdinalIgnoreCase))
            {
                List<PlayerData> top =
                    _storedData.Players
                    .Where(kv => !IsPlayerExcluded(kv.Key))
                    .Select(kv => kv.Value)
                    .OrderByDescending(d => d.PlaytimeSeconds)
                    .Take(_config.TopLeaderboardEntries)
                    .ToList();

                MessagePlayer(player, Lang.Leaderboard_Header);
                int rank = 1;
                foreach (PlayerData entry in top)
                {
                    MessagePlayer(player, Lang.Leaderboard_Entry, rank, entry.Name, FormatTime(entry.PlaytimeSeconds));
                    rank++;
                }

                return;
            }

            MessagePlayer(player, Lang.Error_UnknownSubCommand);
        }

        [ConsoleCommand("playtime")]
        private void cmdConsolePlaytime(ConsoleSystem.Arg arg)
        {
            BasePlayer caller = arg.Player();
            bool isServerConsole = caller == null;

            if (!isServerConsole && !caller.IsAdmin)
            {
                MessagePlayer(caller, Lang.Error_NoPermission);
                return;
            }

            if (arg.Args == null || arg.Args.Length == 0)
            {
                string usage = GetMessage(caller, Lang.Usage_Console_General); ;
                if (isServerConsole)
                    Puts(usage); 
               else
                    SendReply(caller, usage);
                return;
            }

            bool wantReset = arg.GetString(0).Equals("reset", StringComparison.OrdinalIgnoreCase);
            string search;
            if (wantReset)
            {
                if (arg.Args.Length > 1)
                    search = arg.GetString(1);
                else
                    search = null;
            }
            else
                search = arg.GetString(0);

            if (string.IsNullOrEmpty(search))
            {
                string usage = GetMessage(caller, Lang.Usage_Console_Reset); ;
                if (isServerConsole)
                    Puts(usage);
                else
                    SendReply(caller, usage);
                return;
            }

            ulong targetId = 0;
            BasePlayer online = FindPlayerByPartialNameOrId(search);
            if (online != null) targetId = online.userID;
            else if (!ulong.TryParse(search, out targetId))
            {
                string msg = GetMessage(caller, Lang.Error_PlayerNotFound, search);
                if (isServerConsole)
                    Puts(msg); 
               else
                    SendReply(caller, msg);
                return;
            }

            if (wantReset)
            {
                if (_storedData.Players.Remove(targetId))
                {
                    DataFileUtil.Save(DataFileUtil.GetFilePath(), _storedData);

                    string msg = GetMessage(caller, Lang.Info_ResetSuccess, targetId);
                    if (isServerConsole)
                        Puts(msg);
                    else
                        SendReply(caller, msg);
                }
                else
                {
                    string msg = GetMessage(caller, Lang.Error_NoDataStored);
                    if (isServerConsole)
                        Puts(msg);
                    else
                        SendReply(caller, msg);
                }
                return;
            }

            if (!_storedData.Players.TryGetValue(targetId, out PlayerData data))
            {
                string msg = GetMessage(caller, Lang.Error_NoDataForPlayer);
                if (isServerConsole)
                    Puts(msg);
                else
                    SendReply(caller, msg);
                return;
            }

            string result = $"{targetId} {data.Name} {FormatTime(data.PlaytimeSeconds)} (AFK {FormatTime(data.AfkSeconds)})";
            if (isServerConsole)
                Puts(result);
            else
                SendReply(caller, result);
        }

        #endregion Commands

        #region Localization

        private class Lang
        {
            public const string Info_SelfTime = "Info.SelfTime";
            public const string Info_ResetSuccess = "Info.ResetSuccess";
            public const string Help_General = "Help.General";
            public const string Usage_Console_General = "Usage.Console.General";
            public const string Usage_Console_Reset = "Usage.Console.Reset";
            public const string Leaderboard_Header = "Leaderboard.Header";
            public const string Leaderboard_Entry = "Leaderboard.Entry";
            public const string Error_NoPermission = "Error.NoPermission";
            public const string Error_PlayerNotFound = "Error.PlayerNotFound";
            public const string Error_NoDataStored = "Error.NoDataStored";
            public const string Error_NoDataForPlayer = "Error.NoDataForPlayer";
            public const string Error_UnknownSubCommand = "Error.UnknownSubCommand";
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [Lang.Info_SelfTime] = "Playtime: {0} (AFK {1})",
                [Lang.Info_ResetSuccess] = "Reset stats for {0}.",
                [Lang.Help_General] = "Type /playtime top to see the leaders.",
                [Lang.Usage_Console_General] = "Usage: playtime <nameOrId> | playtime reset <nameOrId>",
                [Lang.Usage_Console_Reset] = "Usage: playtime reset <nameOrId>",
                [Lang.Leaderboard_Header] = "Top playtimes:",
                [Lang.Leaderboard_Entry] = "{0}. {1} – {2}",
                [Lang.Error_NoPermission] = "You do not have permission to use this command.",
                [Lang.Error_PlayerNotFound] = "Player '{0}' not found.",
                [Lang.Error_NoDataStored] = "No data stored for that player.",
                [Lang.Error_NoDataForPlayer] = "No data for that player.",
                [Lang.Error_UnknownSubCommand] = "Unknown sub-command."

            }, this, "en");
        }

        private static string GetMessage(BasePlayer player, string messageKey, params object[] args)
        {
            string userId;
            if (player != null)
                userId = player.UserIDString;
            else
                userId = null;

            string message = _plugin.lang.GetMessage(messageKey, _plugin, userId);

            if (args.Length > 0)
                message = string.Format(message, args);

            return message;
        }

        public static void MessagePlayer(BasePlayer player, string messageKey, params object[] args)
        {
            string message = GetMessage(player, messageKey, args);

            if (!string.IsNullOrWhiteSpace(message))
                _plugin.SendReply(player, message);
        }

        #endregion Localization
    }
}

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху