C#:
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.4.7")]
[Description("Rewards players with permission groups based on their activity on the server.")]
public class LoyaltyRewards : CovalencePlugin
{
#region Fields
private Configuration config;
private PersistentData persistentData;
private Dictionary<string, PlayerData> playerData;
#endregion
#region Nested Classes
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<string> PromotionGroups { get; set; }
}
private class PlayerData
{
public HashSet<DateTime> ActiveDays { get; set; } = new HashSet<DateTime>();
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<string, PlayerData> PlayerData { get; set; }
}
#endregion
#region Configuration Methods
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<string> { "endurer", "veteran", "legend" }
};
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
config = Config.ReadObject<Configuration>();
bool saveNeeded = false;
if (config.PromotionGroups == null || config.PromotionGroups.Count == 0)
{
config.PromotionGroups = new List<string> { "endurer", "veteran", "legend" };
saveNeeded = true;
}
if (saveNeeded)
{
SaveConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config);
#endregion
#region Initialization
private void Init()
{
LoadPersistentData();
RegisterPermissions();
ReconcilePlayerData();
if (config.PromoteImmediately)
{
ScheduleDailyPromotionCheck();
}
}
private void LoadPersistentData()
{
persistentData = Interface.Oxide.DataFileSystem.ReadObject<PersistentData>("LoyaltyRewardsData");
playerData = persistentData.PlayerData ?? new Dictionary<string, PlayerData>();
}
private void RegisterPermissions()
{
permission.RegisterPermission("loyaltyrewards.admin", this);
}
private void ReconcilePlayerData()
{
foreach (var group in config.PromotionGroups)
{
if (permission.GroupExists(group))
{
var usersInGroup = permission.GetUsersInGroup(group);
foreach (var userId in usersInGroup)
{
string key = GetPlayerKey(userId);
var player = players.FindPlayerById(userId);
if (player != null)
{
key = $"{player.Id} ({player.Name})";
}
if (playerData.TryGetValue(key, out var data))
{
string oldGroup = data.CurrentGroup;
data.CurrentGroup = ValidateCurrentGroup(data.CurrentGroup);
if (data.CurrentGroup != group)
{
string previousGroup = data.CurrentGroup;
data.CurrentGroup = group;
Puts($"[Reconcile] Player '{player?.Name ?? "Unknown"}' (ID: {player?.Id ?? userId}) CurrentGroup changed from '{previousGroup}' to '{group}' during reconciliation.");
}
}
else
{
playerData[key] = new PlayerData
{
CurrentGroup = group,
ActiveDays = new HashSet<DateTime>(),
PromotedThisPeriod = false
};
Puts($"[Reconcile] Player '{player?.Name ?? "Unknown"}' (ID: {player?.Id ?? userId}) assigned to group '{group}' during reconciliation.");
}
}
}
}
foreach (var playerDataEntry in playerData.Values)
{
string validatedGroup = ValidateCurrentGroup(playerDataEntry.CurrentGroup);
if (playerDataEntry.CurrentGroup != validatedGroup)
{
string oldGroup = playerDataEntry.CurrentGroup;
playerDataEntry.CurrentGroup = validatedGroup;
Puts($"[Reconcile] Player group '{oldGroup}' validated to '{validatedGroup}'.");
}
}
SavePersistentData();
}
private void SavePersistentData()
{
persistentData.PlayerData = playerData;
Interface.Oxide.DataFileSystem.WriteObject("LoyaltyRewardsData", persistentData);
}
#endregion
#region Event Handlers
private void OnServerSave()
{
SavePersistentData();
}
private void OnUserConnected(IPlayer player)
{
string key = GetPlayerKey(player.Id, player.Name);
if (!playerData.TryGetValue(key, out var data))
{
string currentGroup = config.PromotionGroups.FirstOrDefault(g => permission.UserHasGroup(player.Id, g)) ?? "";
data = new PlayerData
{
CurrentGroup = currentGroup,
ActiveDays = new HashSet<DateTime>(),
PromotedThisPeriod = false
};
playerData[key] = data;
SavePersistentData();
Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) connected and assigned to group '{currentGroup}'.");
}
string validatedGroup = ValidateCurrentGroup(data.CurrentGroup);
if (data.CurrentGroup != validatedGroup)
{
string oldGroup = data.CurrentGroup;
data.CurrentGroup = validatedGroup;
Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) CurrentGroup changed from '{oldGroup}' to '{validatedGroup}' during connection validation.");
}
var today = DateTime.Now.Date;
if (data.ActiveDays.Add(today))
{
SavePersistentData();
Puts($"[OnUserConnected] Player '{player.Name}' (ID: {player.Id}) active on {today.ToShortDateString()}.");
}
if (config.PromoteImmediately && !data.PromotedThisPeriod)
{
CheckPlayerPromotion(player, suppressOutput: true, allowDemotions: false); }
}
private void OnPlayerSpawned(BasePlayer player)
{
IPlayer iPlayer = covalence.Players.FindPlayerById(player.UserIDString);
if (iPlayer != null && playerData.TryGetValue(GetPlayerKey(iPlayer), out var data))
{
if (data.PendingPromotionNotification)
{
iPlayer.Message($"Congratulations! Loyalty Rewards has promoted you to {data.CurrentGroup}.");
data.PendingPromotionNotification = false;
SavePersistentData();
Puts($"[OnPlayerSpawned] Sent promotion notification to '{iPlayer.Name}' (ID: {iPlayer.Id}) for group '{data.CurrentGroup}'.");
}
}
}
private void OnNewSave(string filename)
{
if (config.UseWipeCycle)
{
Puts("Detected new save file. Processing promotions and demotions.");
ProcessPromotionsAndDemotions();
ResetPeriod(withPromotions: false); Puts("Period reset. All players' active days and promotion flags have been cleared.");
}
}
#endregion
#region Promotion and Demotion Logic
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 player in players.Connected)
{
OnUserConnected(player);
}
}
private void CheckPlayerPromotion(IPlayer player, bool suppressOutput, bool allowDemotions)
{
string key = GetPlayerKey(player);
if (!playerData.TryGetValue(key, out var data)) return;
if (data.PromotedThisPeriod) return;
if (config.PermissionRequired && !permission.UserHasGroup(player.Id, config.RequiredPermissionGroup))
{
return;
}
int activeDays = data.ActiveDays.Count;
int promotionIndex = config.PromotionGroups.IndexOf(data.CurrentGroup) + 1;
if (activeDays >= config.RequiredDays && promotionIndex < config.PromotionGroups.Count)
{
PromotePlayer(player, data, promotionIndex);
}
else if (allowDemotions)
{
HandleDemotionOrRetention(player, data, 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;
data.PromotedThisPeriod = true;
Puts($"[PromotePlayer] Player '{player.Name}' (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 promotionIndex = config.PromotionGroups.IndexOf(data.CurrentGroup);
if ((DateTime.Now - persistentData.PeriodStartTime).TotalDays < config.MinimumPeriod)
{
return;
}
if (activeDays < config.RequiredDays * config.DemotionThreshold && promotionIndex > 0)
{
DemotePlayer(player, data, promotionIndex);
}
}
private void DemotePlayer(IPlayer player, PlayerData data, int promotionIndex)
{
string oldGroup = data.CurrentGroup;
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 '{player.Name}' (ID: {player.Id}).");
}
if (promotionIndex == 0)
{
DemoteToBaseGroup(player, data);
}
else
{
string previousGroup = config.PromotionGroups[promotionIndex - 1];
AddPlayerToGroup(player, previousGroup);
data.CurrentGroup = previousGroup;
Puts($"[DemotePlayer] Player '{player.Name}' (ID: {player.Id}) demoted to '{previousGroup}' from '{oldGroup}'.");
}
SavePersistentData();
}
private void DemoteToBaseGroup(IPlayer player, PlayerData data)
{
var userGroups = permission.GetUserGroups(player.Id);
var promotionGroupsInPlayer = config.PromotionGroups.Where(group => userGroups.Contains(group)).ToList();
string oldGroup = data.CurrentGroup;
if (!promotionGroupsInPlayer.Any())
{
data.CurrentGroup = "";
Puts($"[DemoteToBaseGroup] Player '{player.Name}' (ID: {player.Id}) demoted to base group from '{oldGroup}'.");
}
else
{
foreach (var group in promotionGroupsInPlayer)
{
permission.RemoveUserGroup(player.Id, group);
Puts($"[DemoteToBaseGroup] Removed group '{group}' from player '{player.Name}' (ID: {player.Id}).");
}
data.CurrentGroup = "";
Puts($"[DemoteToBaseGroup] Player '{player.Name}' (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 userGroups = permission.GetUserGroups(player.Id);
foreach (var group in userGroups)
{
if (config.PromotionGroups.Contains(group) && group != currentGroup)
{
permission.RemoveUserGroup(player.Id, group);
Puts($"[RemoveOnlyManagedGroups] Removed group '{group}' from player '{player.Name}' (ID: {player.Id}) to maintain single group assignment.");
}
}
}
private void ProcessPromotionsAndDemotions()
{
foreach (var player in players.All)
{
CheckPlayerPromotion(player, suppressOutput: true, allowDemotions: true); }
}
#endregion
#region Period Management
[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 <withpromotions|withoutpromotions>");
return;
}
bool withPromotions = args[0].ToLower() == "withpromotions";
ResetPeriod(withPromotions);
if (withPromotions)
{
player.Reply("Loyalty Rewards period has been manually reset with promotions and demotions.");
Puts($"[ResetPeriodCommand] Admin '{player.Name}' (ID: {player.Id}) manually reset the period with promotions and demotions.");
}
else
{
player.Reply("Loyalty Rewards period has been manually reset without promotions and demotions.");
Puts($"[ResetPeriodCommand] Admin '{player.Name}' (ID: {player.Id}) manually reset the period without promotions and demotions.");
}
}
private void ResetPeriod(bool withPromotions)
{
if (withPromotions)
{
ProcessPromotionsAndDemotions();
}
persistentData.PeriodStartTime = DateTime.Now;
foreach (var data in playerData.Values)
{
data.ActiveDays.Clear();
data.PromotedThisPeriod = false;
}
SavePersistentData();
Puts($"[ResetPeriod] Period reset at {DateTime.Now}. Promotions applied: {withPromotions}.");
}
#endregion
#region Helper Methods
private string ValidateCurrentGroup(string currentGroup)
{
if (string.IsNullOrEmpty(currentGroup) || config.PromotionGroups.Contains(currentGroup))
{
return currentGroup; }
else
{
Puts($"[ValidateCurrentGroup] Invalid group '{currentGroup}' detected. Resetting to base group.");
return ""; }
}
private string GetPlayerKey(IPlayer player)
{
return $"{player.Id} ({player.Name})";
}
private string GetPlayerKey(string userId, string userName = "")
{
if (string.IsNullOrEmpty(userName))
{
var player = players.FindPlayerById(userId);
if (player != null)
{
return $"{player.Id} ({player.Name})";
}
}
return $"{userId} ({userName})";
}
#endregion
#region Command Handlers
[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 <player> [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 <player>");
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 promotionIndex = config.PromotionGroups.IndexOf(data.CurrentGroup);
if (promotionIndex < 0)
{
player.Reply($"{target.Name} is already at the base group.");
return;
}
DemotePlayer(target, data, promotionIndex);
if (promotionIndex > 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 demotionThreshold = config.RequiredDays * config.DemotionThreshold;
bool isAboveDemotionThreshold = activeDays >= Math.Ceiling(demotionThreshold);
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 daysNeededForPromotion = config.RequiredDays - activeDays;
if (isAboveDemotionThreshold)
{
player.Reply($"You have played for {activeDays} days, need {daysNeededForPromotion} more days for promotion, and are safe from demotion.");
}
else
{
int daysBelowThreshold = (int)Math.Ceiling(demotionThreshold - activeDays);
player.Reply($"You have played for {activeDays} days, need {daysNeededForPromotion} more days for promotion, and are {daysBelowThreshold} days below the threshold, putting you at risk of demotion.");
}
}
Puts($"[PlaytimeCommand] Player '{player.Name}' (ID: {player.Id}) queried playtime: ActiveDays={activeDays}, PromotedThisPeriod={data.PromotedThisPeriod}.");
}
[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 removedPlayers = 0;
foreach (var playerDataEntry in playerData.ToList())
{
var data = playerDataEntry.Value;
if (!config.PromotionGroups.Contains(data.CurrentGroup) && data.ActiveDays.Count == 0)
{
playerData.Remove(playerDataEntry.Key);
removedPlayers++;
}
}
SavePersistentData();
player.Reply($"Loyalty Rewards data cleaned up. {removedPlayers} players removed.");
Puts($"[CleanupCommand] Admin '{player.Name}' (ID: {player.Id}) performed cleanup. {removedPlayers} players removed from data.");
}
#endregion
}
}