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.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<string, PlayerData> 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<string> PromotionGroups { get; set; }
}
private class PlayerData
{
public string PlayerName { get; set; }
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; }
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<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);
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>();
if (!persistentData.IsMigrated)
{
MigrateOldDataFormat();
persistentData.IsMigrated = true;
SavePersistentData();
}
}
private void RegisterPermissions()
{
permission.RegisterPermission("loyaltyrewards.admin", this);
}
private void MigrateOldDataFormat()
{
var newData = new Dictionary<string, PlayerData>();
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<DateTime>(),
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<DateTime>(),
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 <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.");
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 <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 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.");
}
}
}