C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Oxide.Plugins
{
[Info("Timed Workbench Unlock", "HunterZ", "2.3.0")]
[Description("Provides timed/manual/disabled unlocking of workbenches")]
class TimedWorkbenchUnlock : RustPlugin
{
#region Vars
// ordered list of workbench IDs
private static readonly int[] _WORKBENCH_ID = {
1524187186, -41896755, -1607980696
};
// periodic global status broadcast timer
private Timer _broadcastTimer;
// data managed via config file
private ConfigData _configData;
// "can't craft" warning suppression timers by userID
private readonly Dictionary<ulong, Timer> _craftWarned = new();
// string builder for producing status messages
private readonly StringBuilder _statusBuilder = new();
// workbench unlock announcement broadcast timers
private readonly Timer[] _unlockTimers = { null, null, null };
#region Permission Strings
private const string PERMISSION_ADMIN = "admin";
private const string PERMISSION_BROADCAST = "broadcast";
private const string PERMISSION_INFO = "info";
private const string PERMISSION_MODIFY = "modify";
private const string PERMISSION_RELOAD = "reload";
private const string PERMISSION_RESET = "reset";
private const string PERMISSION_SKIPLOCK = "skiplock";
private const string PERMISSION_WIPE = "wipe";
#endregion Permission Strings
#endregion Vars
#region Utilities
// get time since wipe in seconds
// if positive optional parameter is specified, it is used as a passthrough
private double GetWipeElapsedSeconds(double wipeElapsedSeconds = -1.0)
{
if (null == _configData) return 0;
return wipeElapsedSeconds > 0 ? wipeElapsedSeconds :
(DateTime.UtcNow - _configData.LastWipeUTC).TotalSeconds;
}
// returns unlock status for given workbench index (0-2 => level 1-3)
// -1 => locked forever (requires manual unlock)
// 0 => unlocked
// >0 => number of seconds until auto unlock
//
// wipe elapsed seconds can optionally be specified to avoid repeated
// lookups when calling from a loop
private int GetUnlockStatus(int index, double wipeElapsedSeconds = -1.0)
{
if (null == _configData || index < 0 || index > 2) return 0;
wipeElapsedSeconds = GetWipeElapsedSeconds(wipeElapsedSeconds);
double unlockDelaySeconds = _configData.WBConfig[index];
// check for permanent lock
if (unlockDelaySeconds < 0.0) return -1;
// determine auto unlock status
if (unlockDelaySeconds > 0.0)
{
double unlockSecondsRemaining = unlockDelaySeconds - wipeElapsedSeconds;
return unlockSecondsRemaining > 0 ?
(int)Math.Ceiling(unlockSecondsRemaining) : 0;
}
// unlockDelaySeconds == 0.0 => always unlocked
return 0;
}
// returns an array of unlock times for workbenches
// see GetUnlockTime() for value meanings
private int[] GetUnlockStatus()
{
double wipeElapsedSeconds = GetWipeElapsedSeconds();
// unrolled loop for simplicity
return new[]
{
GetUnlockStatus(0, wipeElapsedSeconds),
GetUnlockStatus(1, wipeElapsedSeconds),
GetUnlockStatus(2, wipeElapsedSeconds)
};
}
// create a timer that will fire when the given workbench should unlock, or
// null if manually locked or already unlocked
private Timer GetTimer(int index, double wipeElapsedSeconds = -1.0)
{
if (index < 0 || index > 2) { return null; }
wipeElapsedSeconds = GetWipeElapsedSeconds(wipeElapsedSeconds);
var status = GetUnlockStatus(index, wipeElapsedSeconds);
if (status > 0) return timer.Once(status, () => { ReportUnlock(index); });
return null;
}
// destroy existing broadcast timer (if any)
private void DestroyBroadcastTimer()
{
if (null == _broadcastTimer) return;
_broadcastTimer.Destroy();
_broadcastTimer = null;
}
// (re)set broadcast timer to fire at configured interval
private void SetBroadcastTimer()
{
DestroyBroadcastTimer();
// only set new timer if config value is positive (i.e.broadcast period
// in seconds)
var broadcastConfig = _configData?.BroadcastConfig ?? 0;
if (broadcastConfig > 0)
{
_broadcastTimer =
timer.Every(broadcastConfig, () => { ReportStatus(null); });
}
}
// destroy all existing timers managed by unlockTimers
private void DestroyUnlockTimers()
{
for (int i = 0; i < _unlockTimers.Length; ++i)
{
var unlockTimer = _unlockTimers[i];
if (null == unlockTimer || unlockTimer.Destroyed) continue;
unlockTimer.Destroy();
_unlockTimers[i] = null;
}
}
// (re)set all unlock announcement timers as appropriate
// this should be called whenever unlock times might have changed
private void SetUnlockTimers()
{
// timers don't auto-destruct, so wipe them to avoid double-firing
DestroyUnlockTimers();
var wipeElapsedSeconds = GetWipeElapsedSeconds();
for (int i = 0; i < _unlockTimers.Length; ++i)
{
_unlockTimers[i] = GetTimer(i, wipeElapsedSeconds);
}
}
private void DestroyWarnTimer(ulong userId)
{
if (_craftWarned.Remove(userId, out var wTimer) &&
false == wTimer?.Destroyed)
{
wTimer.Destroy();
}
}
// generate color locked/unlocked status text for twinfo command
private string UnlockStatusString(int status, IPlayer player) =>
status == 0 ?
Colorize(lang.GetMessage("Unlocked", this, player.Id), "green") :
Colorize(lang.GetMessage("Locked", this, player.Id), "red");
// return true if player is null, server, or admin, or has permission, else
// reply with "no permission" message and return false
private bool HasPermission(IPlayer player, string perm)
{
if (null == player) return false;
var hasPermission =
player.IsServer ||
player.HasPermission(PrefixPermission(PERMISSION_ADMIN)) ||
player.HasPermission(PrefixPermission(perm));
if (!hasPermission) SendMessage(player, "NoPermission");
return hasPermission;
}
// return a prefixed version of the given permission string
// this is done to avoid hard-coding it, which would be a maintenance issue
private string PrefixPermission(string perm) => Name.ToLower() + "." + perm;
// report user-friendly detailed status
private void ReportStatus(IPlayer player)
{
// don't report status if nobody is online
if (null == player && BasePlayer.activePlayerList.IsNullOrEmpty()) return;
var status = GetUnlockStatus();
// don't report status if everything is unlocked
if (0 == status[0] && 0 == status[1] && 0 == status[2]) return;
_statusBuilder.Clear();
_statusBuilder.AppendLine(FormatMessage(player, "StatusBanner"));
for (int index = 0; index < 3; ++index)
{
string wbNumStr = (index + 1).ToString(CultureInfo.CurrentCulture);
switch (status[index])
{
case < 0:
{
_statusBuilder.AppendLine(
FormatMessage(player, "StatusManual", wbNumStr));
}
break;
case 0:
{
_statusBuilder.AppendLine(
FormatMessage(player, "StatusUnlocked", wbNumStr));
}
break;
case > 0:
{
_statusBuilder.AppendLine(FormatMessage(
player, "StatusTime", wbNumStr,
TimeSpan.FromSeconds(status[index]).ToString(
"g", CultureInfo.CurrentCulture)));
}
break;
}
}
SendRawMessage(player, _statusBuilder.ToString());
}
// report that a workbench has unlocked
private void ReportUnlock(int index)
{
if (index < 0 || index > 2) return;
// don't report unlock if nobody is online
if (BasePlayer.activePlayerList.IsNullOrEmpty()) return;
SendMessage(
null, "UnlockNotice", (index + 1).ToString(CultureInfo.CurrentCulture));
}
// return whether crafting of the given item ID should be allowed
private bool AllowCraftAttempt(BasePlayer player, int itemid)
{
// do nothing if any of the following are true:
// - player reference invalid
// - player in Tutorial Island
// - player crafting something other than a workbench
// - player has skiplock permission (note: admins not exempt by default)
if (null == player ||
player.IsInTutorial ||
!_WORKBENCH_ID.Contains(itemid) ||
player.IPlayer.HasPermission(PrefixPermission(PERMISSION_SKIPLOCK)))
{
return true;
}
// get status
var wbIndex = Array.IndexOf(_WORKBENCH_ID, itemid);
var status = GetUnlockStatus(wbIndex);
// do nothing if workbench is unlocked
if (0 == status) return true;
// warn player
if (_configData.ReportAsSound) WarnSound(player);
// abort here if text reports disabled, to avoid building time string
if (status > 0 && !_configData.ReportAsChat && !_configData.ReportAsToast)
{
return false;
}
var timeString = status > 0 ?
TimeSpan.FromSeconds(status).ToString("g", CultureInfo.CurrentCulture) :
null;
if (_configData.ReportAsChat)
{
WarnChat(player, timeString);
}
if (_configData.ReportAsToast)
{
WarnToast(player, timeString);
}
// block crafting
return false;
}
private void WarnChat(BasePlayer player, string timeString = null)
{
// abort if chat spam suppression timer active for player
var userId = player.userID.Get();
if (_craftWarned.ContainsKey(userId)) return;
SendMessage(
player.IPlayer,
null == timeString ? "CannotCraftManual" : "CannotCraft",
timeString);
// set chat spam suppression timer
_craftWarned.Add(userId, timer.Once(5.0f, () =>
{
DestroyWarnTimer(userId);
}));
}
private static void WarnSound(BasePlayer player)
{
Effect.server.Run(
"assets/prefabs/locks/keypad/effects/lock.code.denied.prefab",
player.transform.position);
}
private void WarnToast(BasePlayer player, string timeString = null)
{
SendToast(
player.IPlayer,
null == timeString ? "CannotCraftManual" : "CannotCraft",
timeString);
}
#endregion Utilities
#region Messaging
// load default message text dictionary
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["BroadcastDisabled"] = "Status broadcast disabled",
["BroadcastSet"] = "Status broadcast period set to {0} second(s)",
["CannotCraft"] = "Cannot craft this item (unlocks in {0})",
["CannotCraftManual"] = "Cannot craft this item (unlocks manually/never)",
["InfoBanner"] = "Now @{0} / T1 {1} (@{2}/{3}) / T2 {4} (@{5}/{6}) / T3 {7} (@{8}/{9})",
["InvalidWorkbench"] = "Invalid workbench number specified!",
["Locked"] = "locked",
["ModifiedManual"] = "WB {0} is now always locked",
["ModifiedTime"] = "WB {0} now unlocks in {1} second(s) after wipe",
["ModifiedUnlocked"] = "WB {0} is now always unlocked",
["NoPermission"] = "You don't have permission to use this command",
["PluginWipe"] = "Wipe time reset to {0}",
["ReloadConfig"] = "Config has been reloaded",
["ResetConfig"] = "Config has been reset",
["StatusBanner"] = "Workbenches are currently on a timed unlock system.Current status:",
["StatusManual"] = "- Workbench Level {0}: Unlocks manually/never",
["StatusTime"] = "- Workbench Level {0}: Unlocks in {1}",
["StatusUnlocked"] = "- Workbench Level {0}: Unlocked!",
["SyntaxError"] = "Syntax Error!",
["Unlocked"] = "unlocked",
["UnlockNotice"] = "Workbench Level {0} has unlocked, and can now be crafted!"
}, this);
}
// format a message based on language dictionary, arguments, and destination
private string FormatMessage(
IPlayer player, string langCode, params string[] args)
{
var playerId = null == player || player.IsServer ? null : player.Id;
var msg = string.Format(lang.GetMessage(langCode, this, playerId), args);
// strip color markings out of console messages
if (null == playerId)
// note: cannot supply StringComparison enum value here, as it results
// in a "not implemented" exception in some cases
msg = msg.Replace("<color=red>", string.Empty).Replace("<color=green>", string.Empty).Replace("</color>", string.Empty);
return msg;
}
// send a message to player or server without additional formatting
private void SendRawMessage(IPlayer player, string message)
{
if (null == player)
{
Server.Broadcast(message);
}
else
{
player.Reply(message);
}
}
// send a message to player or server based on language dictionary and
// arguments
// this is the primary method that should be used to communicate to users
private void SendMessage(
IPlayer player, string langCode, params string[] args)
{
SendRawMessage(player, FormatMessage(player, langCode, args));
}
private void SendToast(
IPlayer player, string langCode, params string[] args)
{
var basePlayer = player.Object as BasePlayer;
if (null == basePlayer) return;
basePlayer.ShowToast(0, FormatMessage(player, langCode, args));
}
// decorate a string with color codes
// note that only red or green should be used, as FormatMessage() only
// strips those
private static string Colorize(string str, string color) =>
"<color=" + color + ">" + str + "</color>";
#endregion Messaging
#region Hooks
// called by Oxide after config load
protected void Init()
{
SetBroadcastTimer();
SetUnlockTimers();
// Permissions
permission.RegisterPermission(
PrefixPermission(PERMISSION_ADMIN), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_BROADCAST), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_INFO), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_MODIFY), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_RELOAD), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_RESET), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_SKIPLOCK), this);
permission.RegisterPermission(
PrefixPermission(PERMISSION_WIPE), this);
AddCovalenceCommand("twbroadcast", nameof(CommandBroadcast));
AddCovalenceCommand("twinfo", nameof(CommandInfo));
AddCovalenceCommand("twmodify", nameof(CommandModify));
AddCovalenceCommand("twreload", nameof(CommandReload));
AddCovalenceCommand("twreset", nameof(CommandReset));
AddCovalenceCommand("twwipe", nameof(CommandWipe));
}
// called by Oxide on plugin unload
protected void Unload()
{
// clean up any timers
DestroyBroadcastTimer();
DestroyUnlockTimers();
foreach (var (_, warnTimer) in _craftWarned)
{
if (null == warnTimer || warnTimer.Destroyed) continue;
warnTimer.Destroy();
}
_craftWarned.Clear();
}
private object CanCraft(
PlayerBlueprints playerBlueprints, ItemDefinition itemDefinition) =>
!_configData.BlockCraft || AllowCraftAttempt(
playerBlueprints.baseEntity, itemDefinition.itemid) ? null : false;
private object CanResearchItem(BasePlayer player, Item item) =>
!_configData.BlockResearch || AllowCraftAttempt(
player, item.info.itemid) ? null : false;
private void OnPlayerConnected(BasePlayer player) =>
ReportStatus(player.IPlayer);
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
DestroyWarnTimer(player.userID.Get());
}
#endregion Hooks
#region Commands
private void CommandBroadcast(IPlayer player, string command, string[] args)
{
if (null == _configData || !HasPermission(player, PERMISSION_BROADCAST))
{
return;
}
if (args.Length < 1)
{
player.Reply(string.Format(
lang.GetMessage("SyntaxError", this, player.Id), command));
return;
}
_configData.BroadcastConfig = Convert.ToInt32(args[0]);
if (_configData.BroadcastConfig < 0) _configData.BroadcastConfig = 0;
SaveConfig();
SetBroadcastTimer();
if (_configData.BroadcastConfig > 0)
{
SendMessage(
player, "BroadcastSet", _configData.BroadcastConfig.ToString());
}
else
{
SendMessage(player, "BroadcastDisabled");
}
}
private void CommandInfo(IPlayer player)
{
if (null == _configData || !HasPermission(player, PERMISSION_INFO))
{
return;
}
var status = GetUnlockStatus();
SendMessage(player, "InfoBanner",
GetWipeElapsedSeconds().ToString(CultureInfo.CurrentCulture),
UnlockStatusString(status[0], player),
status[0].ToString(CultureInfo.CurrentCulture),
_configData.WBConfig[0].ToString(CultureInfo.CurrentCulture),
UnlockStatusString(status[1], player),
status[1].ToString(CultureInfo.CurrentCulture),
_configData.WBConfig[1].ToString(CultureInfo.CurrentCulture),
UnlockStatusString(status[2], player),
status[2].ToString(CultureInfo.CurrentCulture),
_configData.WBConfig[2].ToString(CultureInfo.CurrentCulture)
);
}
private void CommandModify(IPlayer player, string command, string[] args)
{
if (null == _configData || !HasPermission(player, PERMISSION_MODIFY))
{
return;
}
if (args.Length < 2)
{
player.Reply(string.Format(
lang.GetMessage("SyntaxError", this, player.Id), command));
return;
}
var wbIndex = Convert.ToInt32(args[0]) - 1;
if (wbIndex < 0 || wbIndex > 2)
{
player.Reply(string.Format(
lang.GetMessage("InvalidWorkbench", this, player.Id), command));
return;
}
var wbConfig = Convert.ToInt32(args[1]);
if (wbConfig < -1) wbConfig = -1;
_configData.WBConfig[wbIndex] = wbConfig;
SaveConfig();
SetUnlockTimers();
if (wbConfig < 0)
{
SendMessage(player, "ModifiedManual", args[0]);
}
else if (wbConfig > 0)
{
SendMessage(player, "ModifiedTime", args[0], wbConfig.ToString(
CultureInfo.CurrentCulture));
}
else
{
SendMessage(player, "ModifiedUnlocked", args[0]);
}
}
private void CommandReload(IPlayer player)
{
if (!HasPermission(player, PERMISSION_RELOAD)) return;
LoadConfig();
SetBroadcastTimer();
SetUnlockTimers();
SendMessage(player, "ReloadConfig");
CommandInfo(player);
}
private void CommandReset(IPlayer player)
{
if (!HasPermission(player, PERMISSION_RESET)) return;
LoadDefaultConfig();
SaveConfig();
SetBroadcastTimer();
SetUnlockTimers();
SendMessage(player, "ResetConfig");
CommandInfo(player);
}
private void CommandWipe(IPlayer player)
{
if (null == _configData || !HasPermission(player, PERMISSION_WIPE))
{
return;
}
DateTime currentTime = DateTime.UtcNow;
_configData.LastWipeUTC = currentTime;
SaveConfig();
SetUnlockTimers();
SendMessage(player, "PluginWipe", currentTime.ToString(
"R", CultureInfo.CurrentCulture));
}
#endregion Commands
#region Configuration
// need to append logic to check for map wipe since last load
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_configData = Config.ReadObject<ConfigData>();
if (null == _configData)
{
throw new JsonException("ReadObject() returned null");
}
}
catch (Exception ex)
{
PrintWarning($"LoadConfig(): Exception while loading configuration file:\n{ex}");
LoadDefaultConfig();
}
if (null == _configData) return;
if (_configData.WBConfig.Length != 3)
{
PrintWarning($"LoadConfig(): Got {_configData.WBConfig.Length} workbench unlock settings, but expected 3; resetting to default list");
_configData.WBConfig = ConfigData._DEFAULT_WB_SECONDS;
}
var serverWipeTime = SaveRestore.SaveCreatedTime;
if (_configData.LastWipeUTC < serverWipeTime)
{
_configData.LastWipeUTC = serverWipeTime;
Puts("LoadConfig(): Wipe detected - reset wipe time to " + serverWipeTime.ToString("R", CultureInfo.CurrentCulture));
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
Puts("LoadDefaultConfig(): Creating a new configuration file");
_configData = new ConfigData();
}
protected override void SaveConfig() => Config.WriteObject(_configData);
// config file data class
private sealed class ConfigData
{
// default workbench unlock times
[JsonIgnore]
public static readonly int[] _DEFAULT_WB_SECONDS = {
86400, 172800, 259200
};
[JsonProperty(PropertyName = "Global status broadcast interval in seconds (0 to disable)")]
public int BroadcastConfig { get; set; } = 300;
[JsonProperty(PropertyName = "Time that current wipe started (UTC)")]
public DateTime LastWipeUTC { get; set; } = SaveRestore.SaveCreatedTime;
[JsonProperty(PropertyName = "Workbench unlock times (seconds from start of wipe, or 0 for unlocked, or -1 for permanently locked)")]
public int[] WBConfig { get; set; } = _DEFAULT_WB_SECONDS;
[JsonProperty(PropertyName = "Block crafting of locked workbench(es)")]
public bool BlockCraft = true;
[JsonProperty(PropertyName = "Block researching of locked workbench(es)")]
public bool BlockResearch = true;
[JsonProperty(PropertyName = "Report craft failure as chat message")]
public bool ReportAsChat = false;
[JsonProperty(PropertyName = "Report craft failure as sound effect")]
public bool ReportAsSound = true;
[JsonProperty(PropertyName = "Report craft failure as toast message")]
public bool ReportAsToast = true;
}
#endregion Configuration
}
}