· 4 years ago · Sep 02, 2021, 08:14 AM
1using Newtonsoft.Json;
2using Oxide.Core;
3using Oxide.Core.Configuration;
4using Oxide.Core.Libraries;
5using Oxide.Core.Libraries.Covalence;
6using Oxide.Core.Plugins;
7using System;
8using System.Collections.Generic;
9
10namespace Oxide.Plugins
11{
12 [Info("PlaytimeTracker", "k1lly0u", "0.2.2")]
13 [Description("Track player time spent on the server")]
14 class PlaytimeTracker : CovalencePlugin
15 {
16 #region Fields
17 private StoredData storedData;
18 private DynamicConfigFile data;
19
20 private static Action<string, double> TimeReward;
21 private static Action<string, string> ReferralReward;
22
23 private static PluginTimers Timer;
24 private static Plugin RewardPlugin;
25 #endregion
26
27 #region Oxide Hooks
28 protected override void LoadDefaultMessages() => lang.RegisterMessages(Messages, this);
29
30 void Init()
31 {
32 LoadData();
33 }
34 private void OnServerInitialized()
35 {
36 TimeReward = IssueReward;
37 ReferralReward = IssueReward;
38
39 Configuration.Reward.RegisterPermissions(this, permission);
40
41 Timer = timer;
42
43 ValididateRewardSystem();
44
45 foreach (IPlayer user in players.Connected)
46 OnUserConnected(user);
47
48 TimedSaveData();
49 }
50
51 private void OnUserConnected(IPlayer user) => storedData.OnUserConnected(user);
52
53 private void OnUserDisconnected(IPlayer user) => storedData.OnUserDisconnected(user);
54
55 private void OnPluginLoaded(Plugin plugin)
56 {
57 if (plugin != null && plugin.Name.Equals(Configuration.Reward.Plugin, StringComparison.OrdinalIgnoreCase))
58 RewardPlugin = plugin;
59 }
60
61 private void Unload()
62 {
63 SaveData();
64
65 TimeReward = null;
66 ReferralReward = null;
67
68 Timer = null;
69 RewardPlugin = null;
70 Configuration = null;
71 }
72 #endregion
73
74 #region Functions
75 private void ValididateRewardSystem()
76 {
77 RewardPlugin = plugins.Find(Configuration.Reward.Plugin);
78
79 if (RewardPlugin == null)
80 PrintError("The selected reward system is not loaded. Unable to issue rewards");
81 }
82
83 private static double CurrentTime => DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds;
84
85 private static string FormatTime(double time)
86 {
87 TimeSpan dateDifference = TimeSpan.FromSeconds((float)time);
88 int days = dateDifference.Days;
89 int hours = dateDifference.Hours;
90 hours += (days * 24);
91 return string.Format("{0:00}h:{1:00}m:{2:00}s", hours, dateDifference.Minutes, dateDifference.Seconds);
92 }
93 #endregion
94
95 #region Rewards
96 internal void IssueReward(string id, double amount)
97 {
98 float multiplier = 1f;
99
100 foreach (KeyValuePair<string, float> kvp in Configuration.Reward.CustomMultipliers)
101 {
102 if (permission.UserHasPermission(id, kvp.Key))
103 {
104 if (kvp.Value > multiplier)
105 multiplier = kvp.Value;
106 }
107 }
108
109 amount *= multiplier;
110
111 switch (Configuration.Reward.Plugin)
112 {
113 case "ServerRewards":
114 RewardPlugin?.Call("AddPoints", ulong.Parse(id), (int)amount);
115 break;
116 case "Economics":
117 RewardPlugin?.Call("Deposit", id, amount);
118 break;
119 default:
120 break;
121 }
122
123 IPlayer user = players.FindPlayerById(id);
124 if (user != null && user.IsConnected)
125 Message(user, $"Reward.Given.{Configuration.Reward.Plugin}", (int)amount);
126 }
127
128 internal void IssueReward(string referrer, string referee)
129 {
130 IssueReward(referrer, Configuration.Reward.Referral.InviteReward);
131 IssueReward(referee, Configuration.Reward.Referral.JoinReward);
132 }
133 #endregion
134
135 #region Commands
136 private double _nextTopUpdate;
137 private List<StoredData.UserData> topList = new List<StoredData.UserData>();
138
139 [Command("playtime")]
140 private void cmdPlaytime(IPlayer user, string command, string[] args)
141 {
142 if (args.Length == 0)
143 {
144 double time = storedData.GetPlayTimeForPlayer(user.Id);
145 double afkTime = storedData.GetAFKTimeForPlayer(user.Id);
146
147 if (time == 0 && afkTime == 0)
148 Message(user, "Error.NoPlaytimeStored");
149 else
150 {
151 if (Configuration.General.TrackAFK)
152 Message(user, "Playtime.Both", FormatTime(time), FormatTime(afkTime));
153 else Message(user, "Playtime.Single", FormatTime(time));
154 }
155
156 Message(user, "Playtime.Help");
157 return;
158 }
159
160 switch (args[0].ToLower())
161 {
162 case "top":
163 string str = lang.GetMessage("Top.Title", this, user.Id);
164
165 if (CurrentTime > _nextTopUpdate)
166 {
167 storedData.GetTopPlayTime(topList);
168 _nextTopUpdate = CurrentTime + 60f;
169 }
170
171 for (int i = 0; i < Math.Min(Configuration.General.TopCount, topList.Count); i++)
172 str += string.Format(lang.GetMessage("Top.Format", this), topList[i].displayName, FormatTime(topList[i].playtime));
173
174 user.Reply(str);
175 return;
176 default:
177 if (user.IsAdmin)
178 {
179 IPlayer target = players.FindPlayer(args[0]);
180 if (target == null)
181 {
182 Message(user, "Error.NoPlayerFound", args[0]);
183 return;
184 }
185 double time = storedData.GetPlayTimeForPlayer(target.Id);
186 if (time == 0)
187 {
188 Message(user, "Error.NoTimeStored");
189 return;
190 }
191
192 user.Reply($"{target.Name} - {FormatTime(time)}");
193 }
194 else Message(user, "Error.InvalidSyntax");
195 break;
196 }
197 }
198
199 [Command("refer")]
200 private void cmdRefer(IPlayer user, string command, string[] args)
201 {
202 if (!Configuration.Reward.Referral.Enabled)
203 {
204 Message(user, "Referral.Disabled");
205 return;
206 }
207
208 if (args.Length == 0)
209 {
210 Message(user, "Referral.Help");
211 return;
212 }
213
214 if (storedData.HasBeenReferred(user.Id))
215 {
216 Message(user, "Referral.Submitted");
217 return;
218 }
219
220 IPlayer referrer = players.FindPlayer(args[0]);
221 if (referrer == null)
222 {
223 Message(user, "Error.NoPlayerFound", args[0]);
224 return;
225 }
226
227 if (referrer.Id.Equals(user.Id))
228 {
229 Message(user, "Referral.Self");
230 return;
231 }
232
233 storedData.ReferPlayer(referrer.Id, user.Id);
234
235 Message(user, "Referral.Accepted");
236
237 if (referrer.IsConnected)
238 Message(referrer, "Referral.Acknowledged", user.Name);
239 }
240 #endregion
241
242 #region API
243 private object GetPlayTime(string id)
244 {
245 double time = storedData.GetPlayTimeForPlayer(id);
246 return time == 0 ? null : (object)time;
247 }
248
249 private object GetAFKTime(string id)
250 {
251 double time = storedData.GetAFKTimeForPlayer(id);
252 return time == 0 ? null : (object)time;
253 }
254
255 private object GetReferrals(string id)
256 {
257 int amount = storedData.GetReferralsForPlayer(id);
258 return amount == 0 ? null : (object)amount;
259 }
260 #endregion
261
262 #region Config
263 private static ConfigData Configuration;
264
265 private class ConfigData
266 {
267 [JsonProperty(PropertyName = "General Options")]
268 public GeneralOptions General { get; set; }
269
270 [JsonProperty(PropertyName = "Reward Options")]
271 public RewardOptions Reward { get; set; }
272
273 public class GeneralOptions
274 {
275 [JsonProperty(PropertyName = "Data save interval (seconds)")]
276 public int SaveInterval { get; set; }
277
278 [JsonProperty(PropertyName = "Track player AFK time")]
279 public bool TrackAFK { get; set; }
280
281 [JsonProperty(PropertyName = "Number of entries to display in the top playtime list")]
282 public int TopCount { get; set; }
283 }
284 public class RewardOptions
285 {
286 [JsonProperty(PropertyName = "Reward plugin (ServerRewards, Economics)")]
287 public string Plugin { get; set; }
288
289 [JsonProperty(PropertyName = "Playtime rewards")]
290 public PlaytimeRewards Playtime { get; set; }
291
292 [JsonProperty(PropertyName = "Referral rewards")]
293 public ReferralRewards Referral { get; set; }
294
295 [JsonProperty(PropertyName = "Custom reward multipliers (permission / multiplier)")]
296 public Hash<string, float> CustomMultipliers { get; set; }
297
298
299 public class PlaytimeRewards
300 {
301 [JsonProperty(PropertyName = "Issue rewards for playtime")]
302 public bool Enabled { get; set; }
303
304 [JsonProperty(PropertyName = "Reward interval (seconds)")]
305 public int Interval { get; set; }
306
307 [JsonProperty(PropertyName = "Reward amount")]
308 public int Reward { get; set; }
309 }
310
311 public class ReferralRewards
312 {
313 [JsonProperty(PropertyName = "Issue rewards for player referrals")]
314 public bool Enabled { get; set; }
315
316 [JsonProperty(PropertyName = "Referrer reward amount")]
317 public int InviteReward { get; set; }
318
319 [JsonProperty(PropertyName = "Referee reward amount")]
320 public int JoinReward { get; set; }
321 }
322
323 [JsonIgnore]
324 private Permission permission;
325
326 internal void RegisterPermissions(CovalencePlugin plugin, Permission permission)
327 {
328 this.permission = permission;
329
330 foreach (string str in CustomMultipliers.Keys)
331 permission.RegisterPermission(str, plugin);
332 }
333 }
334
335 public VersionNumber Version { get; set; }
336 }
337
338 protected override void LoadConfig()
339 {
340 base.LoadConfig();
341 Configuration = Config.ReadObject<ConfigData>();
342
343 if (Configuration.Version < Version)
344 UpdateConfigValues();
345
346 Config.WriteObject(Configuration, true);
347 }
348
349 protected override void LoadDefaultConfig() => Configuration = GetBaseConfig();
350
351 private ConfigData GetBaseConfig()
352 {
353 return new ConfigData
354 {
355 General = new ConfigData.GeneralOptions
356 {
357 SaveInterval = 900,
358 TrackAFK = true,
359 TopCount = 10
360 },
361 Reward = new ConfigData.RewardOptions
362 {
363 Plugin = "Economics",
364 Playtime = new ConfigData.RewardOptions.PlaytimeRewards
365 {
366 Enabled = true,
367 Interval = 3600,
368 Reward = 5
369 },
370 Referral = new ConfigData.RewardOptions.ReferralRewards
371 {
372 Enabled = true,
373 InviteReward = 5,
374 JoinReward = 3
375 },
376 CustomMultipliers = new Hash<string, float>
377 {
378 ["playtimetracker.examplevip1"] = 1.5f,
379 ["playtimetracker.examplevip2"] = 2.0f,
380 }
381 },
382 Version = Version
383 };
384 }
385
386 protected override void SaveConfig() => Config.WriteObject(Configuration, true);
387
388 private void UpdateConfigValues()
389 {
390 PrintWarning("Config update detected! Updating config values...");
391
392 if (Configuration.Version < new VersionNumber(0, 2, 0))
393 Configuration = GetBaseConfig();
394
395 Configuration.Version = Version;
396 PrintWarning("Config update completed!");
397 }
398
399 #endregion
400
401 #region Data Management
402 private void TimedSaveData()
403 {
404 timer.In(Configuration.General.SaveInterval, () =>
405 {
406 SaveData();
407 TimedSaveData();
408 });
409 }
410
411 private void SaveData() => data.WriteObject(storedData);
412
413 private void LoadData()
414 {
415 if (!Interface.Oxide.DataFileSystem.ExistsDatafile("PlaytimeTracker/user_data") && Interface.Oxide.DataFileSystem.ExistsDatafile("PTTracker/playtime_data"))
416 RestoreOldData();
417 else
418 {
419 data = Interface.Oxide.DataFileSystem.GetFile("PlaytimeTracker/user_data");
420 storedData = data.ReadObject<StoredData>();
421 if (storedData == null)
422 storedData = new StoredData();
423 }
424 }
425
426 [Command("ptt.restorenames")]
427 private void FindMissingNames(IPlayer player, string message, string[] args)
428 {
429 if (!player.IsAdmin)
430 return;
431
432 int missing = 0;
433 int restored = 0;
434 foreach (KeyValuePair<string, StoredData.UserData> kvp in storedData._userData)
435 {
436 if (string.IsNullOrEmpty(kvp.Value.displayName))
437 {
438 missing++;
439 IPlayer user = players.FindPlayerById(kvp.Key);
440 if (user != null)
441 {
442 if (!string.IsNullOrEmpty(user.Name))
443 {
444 restored++;
445 kvp.Value.displayName = user.Name;
446 }
447 else kvp.Value.displayName = "Unnamed";
448 }
449 }
450 }
451
452 player.Reply($"Restored {restored}/{missing} names");
453 }
454
455 private class StoredData
456 {
457 [JsonProperty]
458 internal Hash<string, UserData> _userData = new Hash<string, UserData>();
459
460 [JsonProperty]
461 internal HashSet<string> _referredUsers = new HashSet<string>();
462
463 public bool HasBeenReferred(string id) => _referredUsers.Contains(id);
464
465 public void ReferPlayer(string referrer, string referree)
466 {
467 _referredUsers.Add(referree);
468
469 UserData userData;
470 if (!_userData.TryGetValue(referrer, out userData))
471 userData = _userData[referrer] = new UserData();
472
473 userData.referrals += 1;
474
475 ReferralReward(referrer, referree);
476 }
477
478 public void OnUserConnected(IPlayer user)
479 {
480 UserData userData;
481 if (!_userData.TryGetValue(user.Id, out userData))
482 userData = _userData[user.Id] = new UserData();
483
484 userData.OnUserConnected(user);
485 }
486
487 public void OnUserDisconnected(IPlayer user)
488 {
489 UserData userData;
490 if (_userData.TryGetValue(user.Id, out userData))
491 userData.OnUserDisconnected();
492 }
493
494 public double GetPlayTimeForPlayer(string id)
495 {
496 UserData userData;
497 if (!_userData.TryGetValue(id, out userData))
498 return 0;
499
500 return userData.PlayTime;
501 }
502
503 public double GetAFKTimeForPlayer(string id)
504 {
505 UserData userData;
506 if (!_userData.TryGetValue(id, out userData))
507 return 0;
508
509 return userData.AFKTime;
510 }
511
512 public int GetReferralsForPlayer(string id)
513 {
514 UserData userData;
515 if (!_userData.TryGetValue(id, out userData))
516 return 0;
517
518 return userData.referrals;
519 }
520
521 public void GetTopPlayTime(List<UserData> list)
522 {
523 list.Clear();
524 list.AddRange(_userData.Values);
525
526 list.Sort((UserData a, UserData b) =>
527 {
528 return a.playtime.CompareTo(b.playtime) * -1;
529 });
530 }
531
532 internal void InsertData(string id, string displayName, double playTime, double afkTime, double lastReward, int referrals)
533 {
534 UserData userData;
535 if (!_userData.TryGetValue(id, out userData))
536 userData = _userData[id] = new UserData();
537
538 userData.displayName = displayName;
539 userData.playtime = playTime;
540 userData.afkTime = afkTime;
541 userData.lastRewardTime = playTime;
542 userData.referrals = referrals;
543 }
544
545 internal void InsertReferral(string id)
546 {
547 _referredUsers.Add(id);
548 }
549
550 public class UserData
551 {
552 public double playtime;
553 public double afkTime;
554 public double lastRewardTime;
555 public int referrals;
556
557 public string displayName;
558
559 [JsonIgnore]
560 private IPlayer _user;
561
562 [JsonIgnore]
563 private Timer _timer;
564
565 [JsonIgnore]
566 private double _timeStarted;
567
568 [JsonIgnore]
569 private GenericPosition _lastPosition = new GenericPosition();
570
571 private const float TIMER_INTERVAL = 30f;
572
573 public double PlayTime
574 {
575 get
576 {
577 if (_user == null || !_user.IsConnected)
578 return playtime;
579 return playtime + (_user.Position() != _lastPosition ? (CurrentTime - _timeStarted) : 0);
580 }
581 }
582
583 public double AFKTime
584 {
585 get
586 {
587 if (_user == null || !_user.IsConnected)
588 return afkTime;
589 return afkTime + (_user.Position() == _lastPosition ? (CurrentTime - _timeStarted) : 0);
590 }
591 }
592
593 public void OnUserConnected(IPlayer user)
594 {
595 displayName = user.Name;
596
597 _user = user;
598
599 _lastPosition = user.Position();
600
601 if (_timer != null)
602 OnUserDisconnected();
603
604 StartTimer();
605 }
606
607 private void StartTimer()
608 {
609 _timeStarted = CurrentTime;
610 _timer = Timer.In(TIMER_INTERVAL, OnTimerTick);
611 }
612
613 public void OnUserDisconnected()
614 {
615 _timer.Destroy();
616 _timer = null;
617 }
618
619 private void OnTimerTick()
620 {
621 if (_user != null && _user.IsConnected)
622 {
623 if (Configuration.General.TrackAFK && _user != null)
624 {
625 if (EqualPosition(_user.Position(), _lastPosition))
626 afkTime += TIMER_INTERVAL;
627 else playtime += TIMER_INTERVAL;
628
629 _lastPosition = _user.Position();
630 }
631 else playtime += TIMER_INTERVAL;
632
633 if (RewardPlugin != null && Configuration.Reward.Playtime.Enabled)
634 {
635 double rewardMultiplier = (playtime - lastRewardTime) / Configuration.Reward.Playtime.Interval;
636 if (rewardMultiplier >= 1f)
637 {
638 TimeReward(_user.Id, (double)Configuration.Reward.Playtime.Reward * rewardMultiplier);
639 lastRewardTime = playtime;
640 }
641 }
642 }
643
644 StartTimer();
645 }
646
647 private bool EqualPosition(GenericPosition a, GenericPosition b)
648 {
649 if (a == null || b == null)
650 return false;
651
652 return Math.Abs(a.X - b.X) <= (Math.Abs(a.X) + Math.Abs(b.X) + 1) * float.Epsilon &&
653 Math.Abs(a.Y - b.Y) <= (Math.Abs(a.Y) + Math.Abs(b.Y) + 1) * float.Epsilon &&
654 Math.Abs(a.Z - b.Z) <= (Math.Abs(a.Z) + Math.Abs(b.Z) + 1) * float.Epsilon;
655 }
656 }
657 }
658
659 #region Data Converter
660 private void RestoreOldData()
661 {
662 data = Interface.Oxide.DataFileSystem.GetFile("PlaytimeTracker/user_data");
663 storedData = new StoredData();
664
665 LoadOldData();
666
667 foreach (KeyValuePair<string, PlayData.TimeInfo> kvp in playData.timeData)
668 {
669 IPlayer player = players.FindPlayerById(kvp.Key);
670 storedData.InsertData(kvp.Key, player?.Name ?? "Unnamed", kvp.Value.playTime, kvp.Value.afkTime, kvp.Value.lastReward, kvp.Value.referrals);
671 }
672
673 foreach (string str in referData.referrals)
674 storedData.InsertReferral(str);
675
676 if (permData.permissions.Count > 0)
677 {
678 foreach (KeyValuePair<string, float> kvp in permData.permissions)
679 Configuration.Reward.CustomMultipliers[kvp.Key] = kvp.Value;
680
681 Configuration.Reward.RegisterPermissions(this, permission);
682
683 SaveConfig();
684 }
685
686 playData = null;
687 referData = null;
688 permData = null;
689
690 SaveData();
691 }
692
693 private void LoadOldData()
694 {
695 TimeData = Interface.Oxide.DataFileSystem.GetFile("PTTracker/playtime_data");
696 PermissionData = Interface.Oxide.DataFileSystem.GetFile("PTTracker/permission_data");
697 ReferralData = Interface.Oxide.DataFileSystem.GetFile("PTTracker/referral_data");
698
699 playData = TimeData.ReadObject<PlayData>();
700 if (playData == null)
701 playData = new PlayData();
702
703 referData = ReferralData.ReadObject<RefData>();
704 if (referData == null)
705 referData = new RefData();
706
707 permData = PermissionData.ReadObject<PermData>();
708 if (permData == null)
709 permData = new PermData();
710 }
711
712 private PlayData playData;
713 private PermData permData;
714 private RefData referData;
715
716 private DynamicConfigFile TimeData;
717 private DynamicConfigFile PermissionData;
718 private DynamicConfigFile ReferralData;
719
720 private class PlayData
721 {
722 public Dictionary<string, TimeInfo> timeData = new Dictionary<string, TimeInfo>();
723
724 public class TimeInfo
725 {
726 public double playTime;
727 public double afkTime;
728 public double lastReward;
729 public int referrals;
730 }
731 }
732
733 private class PermData
734 {
735 public Dictionary<string, float> permissions = new Dictionary<string, float>();
736 }
737
738 private class RefData
739 {
740 public List<string> referrals = new List<string>();
741 }
742 #endregion
743 #endregion
744
745 #region Localization
746 private void Message(IPlayer user, string key, params object[] args)
747 {
748 if (args == null || args.Length == 0)
749 user.Reply(lang.GetMessage(key, this, user.Id));
750 else user.Reply(string.Format(lang.GetMessage(key, this, user.Id), args));
751 }
752
753 private readonly Dictionary<string, string> Messages = new Dictionary<string, string>
754 {
755 ["Playtime.Both"] = "[#45b6fe]Playtime[/#] : [#ffd479]{0}[/#]\n[#45b6fe]AFK Time[/#] : [#ffd479]{1}[/#]",
756 ["Playtime.Single"] = "[#45b6fe]Playtime[/#] : [#ffd479]{0}[/#]",
757 ["Playtime.Help"] = "You can see the top scoring playtimes by typing [#a1ff46]/playtime top[/#]",
758 ["Top.Title"] = "[#45b6fe]Top Playtimes:[/#]",
759 ["Top.Format"] = "\n[#a1ff46]{0}[/#] - [#ffd479]{1}[/#]",
760
761 ["Referral.Disabled"] = "The referral system is disabled",
762 ["Referral.Help"] = "[#ffd479]/refer <name or ID>[/#] - Add a referral for the specified player",
763 ["Referral.Submitted"] = "You have already submitted your referral",
764 ["Referral.Self"] = "You can not refer yourself",
765 ["Referral.Accepted"] = "Your referral has been accepted",
766 ["Referral.Acknowledged"] = "[#a1ff46]{0}[/#] has acknowledged a referral from you",
767
768 ["Reward.Given.ServerRewards"] = "You have received [#a1ff46]{0} RP[/#] for playing on our server!",
769 ["Reward.Given.Economics"] = "You have received [#a1ff46]{0}[/#] coins for playing on our server!",
770
771 ["Error.NoPlaytimeStored"] = "No playtime has been stored for you yet",
772 ["Error.NoPlayerFound"] = "No player found with the name [#a1ff46]{0}[/#]",
773 ["Error.NoTimeStored"] = "No time stored for the specified player",
774 ["Error.InvalidSyntax"] = "Invalid syntax",
775 };
776 #endregion
777 }
778}
779