using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; namespace Oxide.Plugins { [Info("Loyalty Rewards", "Corpse", "0.5.3")] [Description("Rewards players with permission groups based on their activity on the server.")] public class LoyaltyRewards : CovalencePlugin { private bool debugMode = false; private Configuration config; private PersistentData persistentData; private Dictionary playerData; private class Configuration { public bool UseWipeCycle { get; set; } public int Period { get; set; } public int RequiredDays { get; set; } public double DemotionThreshold { get; set; } public int MinimumPeriod { get; set; } public bool PromoteImmediately { get; set; } public bool PermissionRequired { get; set; } public string RequiredPermissionGroup { get; set; } public List PromotionGroups { get; set; } } private class PlayerData { public string PlayerName { get; set; } public HashSet ActiveDays { get; set; } = new HashSet(); public string CurrentGroup { get; set; } public bool PromotedThisPeriod { get; set; } public bool PendingPromotionNotification { get; set; } } private class PersistentData { public DateTime PeriodStartTime { get; set; } public DateTime LastWipeDate { get; set; } public Dictionary PlayerData { get; set; } public bool IsMigrated { get; set; } } protected override void LoadDefaultConfig() { config = new Configuration { UseWipeCycle = true, Period = 30, RequiredDays = 7, DemotionThreshold = 0.5, MinimumPeriod = 2, PromoteImmediately = true, PermissionRequired = false, RequiredPermissionGroup = "linked", PromotionGroups = new List { "endurer", "veteran", "legend" } }; SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); config = Config.ReadObject(); bool saveNeeded = false; if (config.PromotionGroups == null || config.PromotionGroups.Count == 0) { config.PromotionGroups = new List { "endurer", "veteran", "legend" }; saveNeeded = true; } if (saveNeeded) SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(config); private void Init() { LoadPersistentData(); RegisterPermissions(); ReconcilePlayerData(); if (config.PromoteImmediately) ScheduleDailyPromotionCheck(); } private void LoadPersistentData() { persistentData = Interface.Oxide.DataFileSystem.ReadObject("LoyaltyRewardsData"); playerData = persistentData.PlayerData ?? new Dictionary(); if (!persistentData.IsMigrated) { MigrateOldDataFormat(); persistentData.IsMigrated = true; SavePersistentData(); } } private void RegisterPermissions() { permission.RegisterPermission("loyaltyrewards.admin", this); } private void MigrateOldDataFormat() { var newData = new Dictionary(); foreach (var entry in playerData) { string key = entry.Key; var data = entry.Value; string userId = ExtractUserIdFromKey(key); if (string.IsNullOrEmpty(userId)) continue; if (newData.TryGetValue(userId, out var exist)) { exist.ActiveDays.UnionWith(data.ActiveDays); exist.PromotedThisPeriod |= data.PromotedThisPeriod; exist.PendingPromotionNotification |= data.PendingPromotionNotification; if (GetPromotionIndex(data.CurrentGroup) > GetPromotionIndex(exist.CurrentGroup)) exist.CurrentGroup = data.CurrentGroup; if (!string.IsNullOrEmpty(data.PlayerName)) exist.PlayerName = data.PlayerName; } else { data.PlayerName = data.PlayerName ?? ExtractPlayerNameFromKey(key); newData[userId] = data; } } playerData = newData; persistentData.PlayerData = playerData; } private string ExtractUserIdFromKey(string key) { int bracketIndex = key.IndexOf('('); if (bracketIndex > 0) return key.Substring(0, bracketIndex).Trim(); return key.Trim(); } private string ExtractPlayerNameFromKey(string key) { int startIndex = key.IndexOf('('); int endIndex = key.IndexOf(')', startIndex + 1); if (startIndex >= 0 && endIndex > startIndex) return key.Substring(startIndex + 1, endIndex - startIndex - 1).Trim(); return "Unknown"; } private int GetPromotionIndex(string group) => config.PromotionGroups.IndexOf(group); private void ReconcilePlayerData() { foreach (var group in config.PromotionGroups) { if (!permission.GroupExists(group)) continue; var rawUsers = permission.GetUsersInGroup(group); if (debugMode) Puts($"[Reconcile] Processing group '{group}' with {rawUsers.Length} users."); foreach (var rawId in rawUsers) { string userId = ExtractUserIdFromKey(rawId); if (debugMode) Puts($"[Reconcile] RawUserId='{rawId}', ExtractedUserId='{userId}'"); var ply = players.FindPlayerById(userId); string plyName = ply?.Name ?? permission.GetUserData(userId)?.LastSeenNickname ?? "Unknown"; if (playerData.TryGetValue(userId, out var d)) { if (debugMode) Puts($"[Reconcile] Found existing data for '{userId}'. CurrentGroup='{d.CurrentGroup}'."); string oldGroup = d.CurrentGroup; d.CurrentGroup = ValidateCurrentGroup(d.CurrentGroup); d.PlayerName = plyName; if (d.CurrentGroup != group) { string prevGroup = d.CurrentGroup; d.CurrentGroup = group; Puts($"[Reconcile] Player '{plyName}' (ID: {userId}) CurrentGroup changed from '{prevGroup}' to '{group}' during reconciliation."); } } else { playerData[userId] = new PlayerData { PlayerName = plyName, CurrentGroup = group, ActiveDays = new HashSet(), PromotedThisPeriod = false }; Puts($"[Reconcile] Player '{plyName}' (ID: {userId}) assigned to group '{group}' during reconciliation."); } } } foreach (var data in playerData.Values) data.CurrentGroup = ValidateCurrentGroup(data.CurrentGroup); SavePersistentData(); } private void SavePersistentData() { persistentData.PlayerData = playerData; Interface.Oxide.DataFileSystem.WriteObject("LoyaltyRewardsData", persistentData); } private void OnServerSave() => SavePersistentData(); private void OnUserConnected(IPlayer player) { string key = GetPlayerKey(player); if (!playerData.TryGetValue(key, out var d)) { string grp = config.PromotionGroups.FirstOrDefault(g => permission.UserHasGroup(player.Id, g)) ?? ""; d = new PlayerData { PlayerName = player.Name, CurrentGroup = grp, ActiveDays = new HashSet(), PromotedThisPeriod = false }; playerData[key] = d; SavePersistentData(); Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) connected and assigned to group '{grp}'."); } else { d.PlayerName = player.Name; } string valGroup = ValidateCurrentGroup(d.CurrentGroup); if (d.CurrentGroup != valGroup) { string oldGroup = d.CurrentGroup; d.CurrentGroup = valGroup; Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) CurrentGroup changed from '{oldGroup}' to '{valGroup}' during connection validation."); } var today = DateTime.Now.Date; if (d.ActiveDays.Add(today)) { SavePersistentData(); Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) active on {today.ToShortDateString()}."); } if (config.PromoteImmediately && !d.PromotedThisPeriod) CheckPlayerPromotion(player, true, false); } private void OnPlayerSpawned(BasePlayer player) { IPlayer pl = covalence.Players.FindPlayerById(player.UserIDString); if (pl != null && playerData.TryGetValue(GetPlayerKey(pl), out var data)) { if (data.PendingPromotionNotification) { pl.Message($"Congratulations! Loyalty Rewards has promoted you to {data.CurrentGroup}."); data.PendingPromotionNotification = false; SavePersistentData(); Puts($"[OnPlayerSpawned] Sent promotion notification to '{pl.Name}' (ID: {pl.Id}) for group '{data.CurrentGroup}'."); } } } private void OnNewSave(string filename) { if (config.UseWipeCycle) { Puts("Detected new save file. Processing promotions and demotions."); ProcessPromotionsAndDemotions(); ResetPeriod(false); Puts("Period reset. All players' active days and promotion flags have been cleared."); } } private void ScheduleDailyPromotionCheck() { var now = DateTime.Now; var midnight = new DateTime(now.Year, now.Month, now.Day).AddDays(1); var timeUntilMidnight = midnight - now; timer.Once((float)timeUntilMidnight.TotalSeconds, () => { RunMidnightCheck(); ScheduleDailyPromotionCheck(); }); } private void RunMidnightCheck() { foreach (var p in players.Connected) OnUserConnected(p); } private void CheckPlayerPromotion(IPlayer player, bool suppressOutput, bool allowDemotions) { string key = GetPlayerKey(player); if (!playerData.TryGetValue(key, out var d)) return; if (d.PromotedThisPeriod) return; if (config.PermissionRequired && !permission.UserHasGroup(player.Id, config.RequiredPermissionGroup)) return; int activeDays = d.ActiveDays.Count; int promIndex = config.PromotionGroups.IndexOf(d.CurrentGroup) + 1; if (activeDays >= config.RequiredDays && promIndex < config.PromotionGroups.Count) { PromotePlayer(player, d, promIndex); d.PromotedThisPeriod = true; SavePersistentData(); } else if (allowDemotions) { HandleDemotionOrRetention(player, d, suppressOutput); } } private void PromotePlayer(IPlayer player, PlayerData data, int promotionIndex) { string newGroup = config.PromotionGroups[promotionIndex]; string oldGroup = data.CurrentGroup; AddPlayerToGroup(player, newGroup); RemoveOnlyManagedGroups(player, newGroup); data.CurrentGroup = newGroup; Puts($"[PromotePlayer] Player '{data.PlayerName}' (ID: {player.Id}) promoted from '{oldGroup}' to '{newGroup}'."); if (!player.IsConnected) { data.PendingPromotionNotification = true; } else { player.Message($"Congratulations! Loyalty Rewards has promoted you to {newGroup}."); } SavePersistentData(); } private void HandleDemotionOrRetention(IPlayer player, PlayerData data, bool suppressOutput) { int activeDays = data.ActiveDays.Count; int pIndex = config.PromotionGroups.IndexOf(data.CurrentGroup); if ((DateTime.Now - persistentData.PeriodStartTime).TotalDays < config.MinimumPeriod) return; if (activeDays < config.RequiredDays * config.DemotionThreshold && pIndex >= 0) DemotePlayer(player, data, pIndex); } private void DemotePlayer(IPlayer player, PlayerData data, int promotionIndex) { string oldGroup = data.CurrentGroup; string pName = data.PlayerName ?? player?.Name ?? "Unknown"; if (!string.IsNullOrEmpty(data.CurrentGroup) && permission.UserHasGroup(player.Id, data.CurrentGroup)) { permission.RemoveUserGroup(player.Id, data.CurrentGroup); Puts($"[DemotePlayer] Removed group '{data.CurrentGroup}' from player '{pName}' (ID: {player.Id})."); } if (promotionIndex == 0) { DemoteToBaseGroup(player, data); } else { string prevGroup = config.PromotionGroups[promotionIndex - 1]; AddPlayerToGroup(player, prevGroup); data.CurrentGroup = prevGroup; Puts($"[DemotePlayer] Player '{pName}' (ID: {player.Id}) demoted to '{prevGroup}' from '{oldGroup}'."); } SavePersistentData(); } private void DemoteToBaseGroup(IPlayer player, PlayerData data) { var usrGroups = permission.GetUserGroups(player.Id); var promoGroups = config.PromotionGroups.Where(g => usrGroups.Contains(g)).ToList(); string oldGroup = data.CurrentGroup; string pName = data.PlayerName ?? player?.Name ?? "Unknown"; if (!promoGroups.Any()) { data.CurrentGroup = ""; Puts($"[DemoteToBaseGroup] Player '{pName}' (ID: {player.Id}) demoted to base group from '{oldGroup}'."); } else { foreach (var g in promoGroups) { permission.RemoveUserGroup(player.Id, g); Puts($"[DemoteToBaseGroup] Removed group '{g}' from player '{pName}' (ID: {player.Id})."); } data.CurrentGroup = ""; Puts($"[DemoteToBaseGroup] Player '{pName}' (ID: {player.Id}) demoted to base group."); } } private void AddPlayerToGroup(IPlayer player, string group) { if (!permission.UserHasGroup(player.Id, group)) { permission.AddUserGroup(player.Id, group); Puts($"[AddPlayerToGroup] Added group '{group}' to player '{player.Name}' (ID: {player.Id})."); } } private void RemoveOnlyManagedGroups(IPlayer player, string currentGroup) { var usrGroups = permission.GetUserGroups(player.Id); foreach (var g in usrGroups) { if (config.PromotionGroups.Contains(g) && g != currentGroup) { permission.RemoveUserGroup(player.Id, g); Puts($"[RemoveOnlyManagedGroups] Removed group '{g}' from player '{player.Name}' (ID: {player.Id}) to maintain single group assignment."); } } } private void ProcessPromotionsAndDemotions() { foreach (var p in players.All) CheckPlayerPromotion(p, true, true); } [Command("lr.resetperiod", "loyaltyrewards.admin")] private void ResetPeriodCommand(IPlayer player, string command, string[] args) { if (!player.HasPermission("loyaltyrewards.admin")) { player.Reply("You do not have permission to use this command."); return; } if (args.Length == 0 || (args[0].ToLower() != "withpromotions" && args[0].ToLower() != "withoutpromotions")) { player.Reply("Usage: /lr.resetperiod "); return; } bool withPromotions = args[0].ToLower() == "withpromotions"; ResetPeriod(withPromotions); if (withPromotions) player.Reply("Loyalty Rewards period has been manually reset with promotions and demotions."); else player.Reply("Loyalty Rewards period has been manually reset without promotions and demotions."); } private void ResetPeriod(bool withPromotions) { if (withPromotions) ProcessPromotionsAndDemotions(); persistentData.PeriodStartTime = DateTime.Now; foreach (var d in playerData.Values) { d.ActiveDays.Clear(); d.PromotedThisPeriod = false; } SavePersistentData(); Puts($"[ResetPeriod] Period reset at {DateTime.Now}. Promotions applied: {withPromotions}."); } private string ValidateCurrentGroup(string currentGroup) { if (string.IsNullOrEmpty(currentGroup) || config.PromotionGroups.Contains(currentGroup)) return currentGroup; Puts($"[ValidateCurrentGroup] Invalid group '{currentGroup}' detected. Resetting to base group."); return ""; } private string GetPlayerKey(IPlayer player) => player.Id; private string GetPlayerKey(string userId) => ExtractUserIdFromKey(userId); [Command("lr.promote", "loyaltyrewards.admin")] private void PromoteCommand(IPlayer player, string command, string[] args) { if (!player.HasPermission("loyaltyrewards.admin")) { player.Reply("You do not have permission to use this command."); return; } if (args.Length < 1) { player.Reply("Usage: /lr.promote [levels] [keeppromotioneligibility]"); return; } var target = players.FindPlayer(args[0]); if (target == null) { player.Reply($"Player '{args[0]}' not found."); return; } string key = GetPlayerKey(target); if (!playerData.TryGetValue(key, out var data)) { player.Reply($"No data found for player '{target.Name}'."); return; } int levels = 1; bool keepPromotionEligibility = false; if (args.Length > 1) { if (int.TryParse(args[1], out int parsedLevels)) levels = parsedLevels; else if (args[1].ToLower() == "keeppromotioneligibility") keepPromotionEligibility = true; } if (args.Length > 2 && args[2].ToLower() == "keeppromotioneligibility") keepPromotionEligibility = true; int promotionIndex = config.PromotionGroups.IndexOf(data.CurrentGroup) + 1; int targetIndex = promotionIndex + levels - 1; if (targetIndex >= config.PromotionGroups.Count) { targetIndex = config.PromotionGroups.Count - 1; Puts($"[PromoteCommand] Player {target.Name} specified promotion levels exceed available groups. Promoting to highest group: {config.PromotionGroups[targetIndex]}."); } PromotePlayer(target, data, targetIndex); if (!keepPromotionEligibility) data.PromotedThisPeriod = true; player.Reply($"{target.Name} has been promoted to {data.CurrentGroup}."); Puts($"[PromoteCommand] Admin '{player.Name}' (ID: {player.Id}) promoted player '{target.Name}' (ID: {target.Id}) to '{data.CurrentGroup}'. Levels: {levels}, Keep Eligibility: {keepPromotionEligibility}."); } [Command("lr.demote", "loyaltyrewards.admin")] private void DemoteCommand(IPlayer player, string command, string[] args) { if (!player.HasPermission("loyaltyrewards.admin")) { player.Reply("You do not have permission to use this command."); return; } if (args.Length < 1) { player.Reply("Usage: /lr.demote "); return; } var target = players.FindPlayer(args[0]); if (target == null) { player.Reply($"Player '{args[0]}' not found."); return; } string key = GetPlayerKey(target); if (!playerData.TryGetValue(key, out var data)) { player.Reply($"No data found for player '{target.Name}'."); return; } int index = config.PromotionGroups.IndexOf(data.CurrentGroup); if (index < 0) { player.Reply($"{target.Name} is already at the base group."); return; } DemotePlayer(target, data, index); if (index > 0) player.Reply($"{target.Name} has been demoted to {data.CurrentGroup}."); Puts($"[DemoteCommand] Admin '{player.Name}' (ID: {player.Id}) demoted player '{target.Name}' (ID: {target.Id}) to '{data.CurrentGroup}'."); } [Command("lr.playtime")] private void PlaytimeCommand(IPlayer player, string command, string[] args) { string key = GetPlayerKey(player); if (!playerData.TryGetValue(key, out var data)) { player.Reply("Your playtime data is not available."); return; } if (config.PermissionRequired && !permission.UserHasGroup(player.Id, config.RequiredPermissionGroup)) { player.Reply($"You are not eligible for promotions because you are not a member of the required group '{config.RequiredPermissionGroup}'."); return; } int activeDays = data.ActiveDays.Count; double demoThreshold = config.RequiredDays * config.DemotionThreshold; bool aboveThreshold = activeDays >= Math.Ceiling(demoThreshold); string periodLabel = config.UseWipeCycle ? "wipe" : "period"; if (data.PromotedThisPeriod) { player.Reply($"You have already been promoted this {periodLabel} and are not eligible for further promotions, but you are safe from demotion."); return; } if (activeDays >= config.RequiredDays) { player.Reply($"You have played for {activeDays} days, are eligible for promotion, and are safe from demotion."); } else { int needed = config.RequiredDays - activeDays; if (aboveThreshold) { player.Reply($"You have played for {activeDays} days, need {needed} more days for promotion, and are safe from demotion."); } else { int below = (int)Math.Ceiling(demoThreshold - activeDays); player.Reply($"You have played for {activeDays} days, need {needed} more days for promotion, and are {below} days below the threshold, putting you at risk of demotion."); } } } [Command("lr.cleanup", "loyaltyrewards.admin")] private void CleanupCommand(IPlayer player, string command, string[] args) { if (!player.HasPermission("loyaltyrewards.admin")) { player.Reply("You do not have permission to use this command."); return; } if (args.Length != 1 || args[0].ToLower() != "ihaveabackup") { player.Reply("Usage: /lr.cleanup ihaveabackup"); return; } int removed = 0; foreach (var entry in playerData.ToList()) { var d = entry.Value; if (!config.PromotionGroups.Contains(d.CurrentGroup) && d.ActiveDays.Count == 0) { playerData.Remove(entry.Key); removed++; } } SavePersistentData(); player.Reply($"Loyalty Rewards data cleaned up. {removed} players removed."); } } }