C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("AFK API", "2CHEVSKII", "1.0.1")]
[Description("Complex developer API for 'Away from keyboard' players.")]
class AFKAPI : CovalencePlugin
{
#region -Fields-
const string BEEP_SOUND_PREFAB = "assets/prefabs/tools/pager/effects/beep.prefab";
#region [Permissions]
const string PERMISSION_USE = "afkapi.use";
const string PERMISSION_KICK = "afkapi.kick";
#endregion
#region [Storage]
IEnumerable<IPlayer> AFKPlayers
{
get
{
return trackedPlayers.Where(kv => kv.Value.IsAFK).Select(kv => kv.Key);
}
}
static AFKAPI instance;
Settings settings;
Dictionary<IPlayer, AFKPlayer> trackedPlayers;
#endregion
#endregion
#region -Configuration-
protected override void LoadDefaultConfig()
{
settings = Settings.Default;
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(settings, true);
protected override void LoadConfig()
{
base.LoadConfig();
try
{
settings = Config.ReadObject<Settings>();
if (settings == null)
{
throw new JsonException("Unable to read configuration file, it will be reset.");
}
}
catch
{
LoadDefaultConfig();
}
}
class Settings
{
public static Settings Default
{
get
{
return new Settings
{
GeneralSettings = new Settings.GeneralPluginSettings
{
SecondsToAFKStatus = 300,
StatusRefreshInterval = 5,
AllowSetupThroughAPI = true
},
CompareSettings = new Settings.ComparePluginSettings
{
CompareBuild = true,
CompareCommunication = true,
CompareItemActions = true,
CompareRotation = true
},
NotificationSettings = new Settings.NotificationPluginSettings
{
NotifyPlayer = true,
NotifyPlayerSound = true,
NotifyPlayerTime = 60
}
};
}
}
[JsonProperty(PropertyName = "General Settings")]
public GeneralPluginSettings GeneralSettings { get; set; }
[JsonProperty(PropertyName = "Accuracy Settings")]
public ComparePluginSettings CompareSettings { get; set; }
[JsonProperty(PropertyName = "Notification Settings")]
public NotificationPluginSettings NotificationSettings { get; set; }
public class GeneralPluginSettings
{
[JsonProperty(PropertyName = "Seconds to consider player is AFK")]
public int SecondsToAFKStatus { get; set; }
[JsonProperty(PropertyName = "AFK Check Interval")]
public int StatusRefreshInterval { get; set; }
[JsonProperty(PropertyName = "Allow other plugins change settings of this API")]
public bool AllowSetupThroughAPI { get; set; }
}
public class ComparePluginSettings
{
[JsonProperty(PropertyName = "Check rotation in pair with position (more accurate)")]
public bool CompareRotation { get; set; }
[JsonProperty(PropertyName = "Check for build attempts")]
public bool CompareBuild { get; set; }
[JsonProperty(PropertyName = "Check for communication attempts (chat/voice)")]
public bool CompareCommunication { get; set; }
[JsonProperty(PropertyName = "Check for item actions (craft/change/use/move)")]
public bool CompareItemActions { get; set; }
}
public class NotificationPluginSettings
{
[JsonProperty(PropertyName = "Notify player before considering him AFK")]
public bool NotifyPlayer { get; set; }
[JsonProperty(PropertyName = "Notify player X seconds before considering him AFK")]
public int NotifyPlayerTime { get; set; }
[JsonProperty(PropertyName = "Notify with sound")]
public bool NotifyPlayerSound { get; set; }
}
}
#endregion
#region -Localization-
const string M_CHAT_PREFIX = "Plugin prefix",
M_PLAYER_AFK_STATUS = "Player AFK status",
M_ERROR_NO_ARGS = "No valid arguments error",
M_OFFLINE_STATUS = "Offline status",
M_IS_AFK_STATUS = "Is AFK status",
M_IS_NOT_AFK_STATUS = "Is not AFK status",
M_AFK_PLAYER_LIST = "AFK player list",
M_NO_AFK_PLAYERS_FOUND = "No players AFK atm",
M_NOTIFICATION = "Notification for AFK player",
M_NO_PERMISSION = "No permission",
M_KICK_REASON = "Kick reason";
readonly Dictionary<string, string> defmessages = new Dictionary<string, string>
{
{ M_CHAT_PREFIX, "<color=#6797e5>[AFK API]</color> " },
{ M_PLAYER_AFK_STATUS, "The player <color=#fff268>{0}</color> AFK status: {1}" },
{ M_ERROR_NO_ARGS, "<color=orange>Wrong command usage, no valid arguments specified</color>" },
{ M_OFFLINE_STATUS, "<color=red>OFFLINE</color>" },
{ M_IS_AFK_STATUS, "<color=yellow>IS AFK</color>" },
{ M_IS_NOT_AFK_STATUS, "<color=lime>NOT AFK</color>" },
{ M_AFK_PLAYER_LIST, "<color=yellow>AFK player list:</color>\n{0}" },
{ M_NO_AFK_PLAYERS_FOUND, "<color=lime>Currently no players are AFK</color>" },
{ M_NOTIFICATION, "<color=red>Start moving, or you will be punished for AFK!</color>" },
{ M_NO_PERMISSION, "<color=red>You have no permission to run this command!</color>" },
{ M_KICK_REASON, "AFK" }
};
protected override void LoadDefaultMessages() => lang.RegisterMessages(defmessages, this, "en");
void MessagePlayer(IPlayer player, string msg, bool prefix = true, params object[] args)
{
if (prefix)
{
player.Message(lang.GetMessage(M_CHAT_PREFIX, this, player.Id) + string.Format(lang.GetMessage(msg, this, player.Id), args));
}
else
{
player.Message(string.Format(lang.GetMessage(msg, this, player.Id), args));
}
}
#endregion
#region -API-
bool IsPlayerAFK(ulong id)
{
return IsPlayerAFK(id.ToString());
}
bool IsPlayerAFK(string id)
{
var player = players.FindPlayerById(id);
if (player == null)
{
throw new Exception($"Player with id {id} not found.");
}
return IsPlayerAFK(player);
}
bool IsPlayerAFK(IPlayer player)
{
if (player == null)
{
throw new ArgumentNullException("Player cannot be null.");
}
var list = AFKPlayers;
if (list.Count() == 0)
{
return false;
}
return list.Contains(player);
}
long GetPlayerAFKTime(ulong id)
{
return (long)GetPlayerAFKTime(id.ToString());
}
float GetPlayerAFKTime(string id)
{
var player = players.FindPlayerById(id);
if (player == null)
{
throw new Exception($"Player with id {id} not found.");
}
return GetPlayerAFKTime(player);
}
float GetPlayerAFKTime(IPlayer player)
{
if (player == null)
{
throw new ArgumentNullException("Player cannot be null");
}
if (!player.IsConnected)
{
return -1f;
}
var comp = trackedPlayers[player];
if (!comp.IsAFK)
{
return -1f;
}
return comp.TimeAFK;
}
List<BasePlayer> GetAFKPlayers()
{
return AFKPlayers.Select(p => (BasePlayer)p.Object).ToList();
}
bool AFKAPI_Setup(string newSettings, bool needToSave = false)
{
if (!settings.GeneralSettings.AllowSetupThroughAPI)
return false;
Settings _newSettings = JsonConvert.DeserializeObject<Settings>(newSettings);
if (_newSettings != null)
{
_newSettings.GeneralSettings.AllowSetupThroughAPI = true;
settings = _newSettings;
if (needToSave)
SaveConfig();
CheckHookSubscriptions();
return true;
}
else
return false;
}
#endregion
#region -Helpers-
void CheckHookSubscriptions()
{
if (!settings.CompareSettings.CompareBuild)
{
Unsubscribe(nameof(CanBuild));
}
if (!settings.CompareSettings.CompareCommunication)
{
Unsubscribe(nameof(OnPlayerChat));
Unsubscribe(nameof(OnPlayerVoice));
}
if (!settings.CompareSettings.CompareItemActions)
{
Unsubscribe(nameof(CanCraft));
Unsubscribe(nameof(OnActiveItemChanged));
Unsubscribe(nameof(OnItemAction));
Unsubscribe(nameof(CanMoveItem));
}
}
#endregion
#region -Oxide hooks-
void Init()
{
instance = this;
permission.RegisterPermission(PERMISSION_USE, this);
permission.RegisterPermission(PERMISSION_KICK, this);
AddUniversalCommand("isafk", nameof(CmdIsAFK));
AddUniversalCommand("getafk", nameof(CmdGetAFK));
AddUniversalCommand("kickafk", nameof(CmdKickAFK));
trackedPlayers = new Dictionary<IPlayer, AFKPlayer>();
}
void OnServerInitialized()
{
CheckHookSubscriptions();
foreach (var player in players.Connected)
{
OnUserConnected(player);
}
}
void Unload()
{
foreach (var p in trackedPlayers.Keys)
{
OnUserDisconnected(p);
}
instance = null;
}
void OnUserConnected(IPlayer player)
{
var basePlayer = (BasePlayer)player.Object;
var comp = basePlayer.gameObject.AddComponent<AFKPlayer>();
}
void OnUserDisconnected(IPlayer player)
{
UnityEngine.Object.Destroy(trackedPlayers[player]);
}
void CanBuild(Planner planner)
{
BasePlayer player = planner.GetOwnerPlayer();
if (player != null)
trackedPlayers[player.IPlayer].OnAction();
}
void CanCraft(ItemCrafter itemCrafter)
{
var player = itemCrafter.baseEntity;
if (player != null)
trackedPlayers[player.IPlayer].OnAction();
}
void OnActiveItemChanged(BasePlayer player)
{
if (player?.IPlayer == null)
{
return;
}
trackedPlayers[player.IPlayer].OnAction();
}
void OnPlayerChat(BasePlayer player)
{
if (player?.IPlayer == null)
{
return;
}
trackedPlayers[player.IPlayer].OnAction();
}
void OnPlayerVoice(BasePlayer player)
{
if (player?.IPlayer == null)
{
return;
}
trackedPlayers[player.IPlayer].OnAction();
}
void OnItemAction(Item item, string action, BasePlayer player)
{
if (player?.IPlayer == null)
{
return;
}
trackedPlayers[player.IPlayer].OnAction();
}
void CanMoveItem(Item item, PlayerInventory playerLoot, uint targetContainer, int targetSlot, int amount)
{
var player = playerLoot.baseEntity;
if (player?.IPlayer == null)
{
return;
}
trackedPlayers[player.IPlayer].OnAction();
}
#endregion
#region -Commands-
void CmdIsAFK(IPlayer player, string command, string[] args)
{
if (!player.HasPermission(PERMISSION_USE))
{
MessagePlayer(player, M_NO_PERMISSION);
}
else if (args.Length == 0)
{
MessagePlayer(player, M_ERROR_NO_ARGS);
}
else
{
var tPlayer = BasePlayer.Find(args[0]);
if (!tPlayer || !trackedPlayers.ContainsKey(tPlayer.IPlayer))
{
MessagePlayer(player, M_PLAYER_AFK_STATUS, true, args[0], M_OFFLINE_STATUS);
}
else
{
var comp = trackedPlayers[tPlayer.IPlayer];
if (comp.IsAFK)
{
MessagePlayer(player, M_PLAYER_AFK_STATUS, true, tPlayer.displayName, M_IS_AFK_STATUS);
}
else
{
MessagePlayer(player, M_PLAYER_AFK_STATUS, true, tPlayer.displayName, M_IS_NOT_AFK_STATUS);
}
}
}
}
void CmdGetAFK(IPlayer player, string command, string[] args)
{
if (!player.HasPermission(PERMISSION_USE))
{
MessagePlayer(player, M_NO_PERMISSION);
}
else
{
var list = AFKPlayers;
if (list.Count() == 0)
{
MessagePlayer(player, M_NO_AFK_PLAYERS_FOUND);
}
else
{
MessagePlayer(player, M_AFK_PLAYER_LIST, false, string.Join("\n", list.Select(p => p.Name)));
}
}
}
void CmdKickAFK(IPlayer player, string command, string[] args)
{
if (!player.HasPermission(PERMISSION_KICK))
{
MessagePlayer(player, M_NO_PERMISSION);
}
else
{
foreach (var afkPlayer in AFKPlayers)
{
afkPlayer.Kick(lang.GetMessage(M_KICK_REASON, this, afkPlayer.Id));
}
}
}
#endregion
#region -Nested types-
class AFKPlayer : MonoBehaviour
{
#region [Fields]
public bool IsAFK
{
get
{
return TimeSinceLastAction >= instance.settings.GeneralSettings.SecondsToAFKStatus;
}
}
public float TimeAFK
{
get
{
return Mathf.Max(0f, TimeSinceLastAction - instance.settings.GeneralSettings.SecondsToAFKStatus);
}
}
float TimeSinceLastAction => Time.realtimeSinceStartup - lastActionTime;
BasePlayer player;
float lastActionTime;
Vector3 lastPosition;
Quaternion lastRotation;
Dictionary<IPlayer, AFKPlayer> trackedPlayers;
#endregion
#region [Public methods]
public void OnAction()
{
lastActionTime = Time.realtimeSinceStartup;
}
#endregion
#region [Lifecycle hooks]
void Start()
{
trackedPlayers = instance.trackedPlayers;
player = GetComponent<BasePlayer>();
trackedPlayers[player.IPlayer] = this;
lastPosition = player.transform.position;
lastRotation = player.eyes.rotation;
InvokeRepeating(nameof(CheckPosition), instance.settings.GeneralSettings.StatusRefreshInterval, instance.settings.GeneralSettings.StatusRefreshInterval);
InvokeRepeating(nameof(CheckNotification), instance.settings.GeneralSettings.StatusRefreshInterval, instance.settings.GeneralSettings.StatusRefreshInterval);
}
void OnDestroy()
{
CancelInvoke();
trackedPlayers.Remove(player.IPlayer);
}
#endregion
#region [Position/rotation checks]
void CheckPosition()
{
if (player.transform.position == lastPosition)
{
if (instance.settings.CompareSettings.CompareRotation && RotationChanged())
OnAction();
}
else
{
lastPosition = player.transform.position;
OnAction();
}
}
bool RotationChanged()
{
if (player.eyes.rotation != lastRotation)
{
lastRotation = player.eyes.rotation;
return true;
}
return false;
}
#endregion
#region [Notifications]
void CheckNotification()
{
if (!instance.settings.NotificationSettings.NotifyPlayer)
{
return;
}
var tsla = TimeSinceLastAction;
if (instance.settings.GeneralSettings.SecondsToAFKStatus - TimeSinceLastAction > instance.settings.NotificationSettings.NotifyPlayerTime)
{
return;
}
NotifyPlayer();
if (instance.settings.NotificationSettings.NotifyPlayerSound)
{
DoBeepEffect();
}
}
void NotifyPlayer()
{
instance.MessagePlayer(player.IPlayer, M_NOTIFICATION);
}
void DoBeepEffect()
{
var beep = new Effect();
beep.Init(Effect.Type.Generic, player.transform.position, Vector3.zero);
beep.pooledString = BEEP_SOUND_PREFAB;
EffectNetwork.Send(beep, player.Connection);
}
#endregion
}
#endregion
}
}