C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Tides" , "Krungh Crow" , "1.0.0")]
[Description("Automated Tidal System.")]
public class Tides : RustPlugin
{
#region variables
private const string PermAdmin = "tides.admin";
private const float MinOceanLevel = 0f;
private const bool StartRising = true;
private const float ChangeThreshold = 0.0001f;
private PluginConfig _config;
private Timer _updateTimer;
private bool _initialized;
private bool _paused;
private bool _manualOverrideActive;
private bool _rising;
private float _manualOceanLevel;
private float _currentOceanLevel;
private float _lastAppliedOceanLevel = float.MinValue;
private float _holdUntilRealtime;
private TideState _tideState = TideState.Rising;
private enum TideState
{
Rising = 0,
HighTide = 1,
Falling = 2,
LowTide = 3
}
#endregion
#region Config
private class PluginConfig
{
[JsonProperty("Tides Enabled")]
public bool TidesEnabled = true;
[JsonProperty("Update Interval Seconds")]
public float UpdateIntervalSeconds = 1f;
[JsonProperty("Increment Amount Per Tick")]
public float IncrementAmountPerTick = 0.005f;
[JsonProperty("Maximum Tide Level")]
public float MaximumTideLevel = 1.15f;
[JsonProperty("Time Between Tides Seconds")]
public float TimeBetweenTidesSeconds = 600f;
[JsonProperty("Send Tide Milestone Messages")]
public bool SendTideMilestones = false;
[JsonProperty("Debug")]
public bool Debug = false;
[JsonProperty("Plugin Version (Do not edit)")]
public string Version = "1.0.0";
}
protected override void LoadDefaultConfig()
{
_config = new PluginConfig
{
Version = Version.ToString()
};
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<PluginConfig>();
if (_config == null)
throw new Exception("Config file is null.");
}
catch
{
PrintWarning("Config file was invalid, creating a new one with defaults.");
LoadDefaultConfig();
}
ValidateConfig();
_config.Version = Version.ToString();
SaveConfig();
}
protected override void SaveConfig()
{
Config.WriteObject(_config , true);
}
private void ValidateConfig()
{
_config.UpdateIntervalSeconds = Mathf.Max(0.05f , _config.UpdateIntervalSeconds);
_config.IncrementAmountPerTick = Mathf.Max(0.0001f , _config.IncrementAmountPerTick);
_config.MaximumTideLevel = Mathf.Max(MinOceanLevel , _config.MaximumTideLevel);
_config.TimeBetweenTidesSeconds = Mathf.Max(0f , _config.TimeBetweenTidesSeconds);
}
#endregion
#region Lang
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string , string>
{
["Prefix"] = "[Tides]" ,
["NoPermission"] = "You don't have permission to use this command." ,
["Usage"] = "Usage: tide pause | resume | set <value> | clear | max <value> | step <value> | interval <seconds> | hold <seconds> | rising | falling" ,
["TidesPaused"] = "Tides paused." ,
["TidesResumed"] = "Tides resumed." ,
["ManualSet"] = "Manual ocean level override enabled: {0:0.###}." ,
["ManualCleared"] = "Manual override cleared. Incremental tides resumed." ,
["MaxSet"] = "Maximum tide level set to {0:0.###}." ,
["StepSet"] = "Increment amount per tick set to {0:0.###}." ,
["IntervalSet"] = "Update interval set to {0:0.###} seconds." ,
["HoldSet"] = "Time between tides set to {0:0.###} seconds." ,
["DirectionRising"] = "Tide direction set to rising." ,
["DirectionFalling"] = "Tide direction set to falling." ,
["InvalidValue"] = "Invalid value." ,
["ReachedHigh"] = "High tide reached. Ocean level: {0:0.0}" ,
["ReachedLow"] = "Low tide reached. Ocean level: {0:0.0}"
} , this);
}
private string Msg(string key , string userId = null , params object[] args)
{
string message = lang.GetMessage(key , this , userId);
return args == null || args.Length == 0 ? message : string.Format(message , args);
}
private string GetPrefix(string userId = null)
{
return Msg("Prefix" , userId);
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermAdmin , this);
LoadConfig();
}
private void OnServerInitialized()
{
_initialized = true;
_rising = StartRising;
_currentOceanLevel = ClampOcean(GetCurrentOceanLevel());
_holdUntilRealtime = 0f;
if (_currentOceanLevel <= MinOceanLevel)
SetTideState(_rising ? TideState.Rising : TideState.LowTide , true);
else if (_currentOceanLevel >= _config.MaximumTideLevel)
SetTideState(_rising ? TideState.HighTide : TideState.Falling , true);
else
SetTideState(_rising ? TideState.Rising : TideState.Falling , true);
if (_config.Debug)
Puts($"Initialized. Version={_config.Version}, CurrentOcean={_currentOceanLevel:0.###}, Max={_config.MaximumTideLevel:0.###}, Hold={_config.TimeBetweenTidesSeconds:0.###}, State={_tideState}");
StartUpdater();
}
private void Unload()
{
DestroyUpdater();
SetOceanLevel(0f);
}
#endregion
#region Commands
[Command("tide")]
private void TideCommand(IPlayer player , string command , string[] args)
{
if (player == null)
return;
if (!player.IsServer && !player.HasPermission(PermAdmin))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("NoPermission" , player.Id)}");
return;
}
if (args.Length == 0)
{
player.Reply($"{GetPrefix(player.Id)} {Msg("Usage" , player.Id)}");
return;
}
switch (args[0].ToLower())
{
case "pause":
_paused = true;
player.Reply($"{GetPrefix(player.Id)} {Msg("TidesPaused" , player.Id)}");
return;
case "resume":
_paused = false;
_manualOverrideActive = false;
player.Reply($"{GetPrefix(player.Id)} {Msg("TidesResumed" , player.Id)}");
ApplyCurrentMode(true);
return;
case "set":
{
float value;
if (args.Length < 2 || !float.TryParse(args[1] , out value))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("InvalidValue" , player.Id)}");
return;
}
value = ClampOcean(value);
_manualOverrideActive = true;
_manualOceanLevel = value;
_currentOceanLevel = value;
_holdUntilRealtime = 0f;
if (_currentOceanLevel <= MinOceanLevel)
SetTideState(TideState.LowTide);
else if (_currentOceanLevel >= _config.MaximumTideLevel)
SetTideState(TideState.HighTide);
else
SetTideState(_rising ? TideState.Rising : TideState.Falling);
player.Reply($"{GetPrefix(player.Id)} {Msg("ManualSet" , player.Id , value)}");
ApplyCurrentMode(true);
return;
}
case "clear":
_manualOverrideActive = false;
_paused = false;
_holdUntilRealtime = 0f;
SetTideState(_rising ? TideState.Rising : TideState.Falling);
player.Reply($"{GetPrefix(player.Id)} {Msg("ManualCleared" , player.Id)}");
ApplyCurrentMode(true);
return;
case "max":
{
float value;
if (args.Length < 2 || !float.TryParse(args[1] , out value))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("InvalidValue" , player.Id)}");
return;
}
_config.MaximumTideLevel = Mathf.Max(MinOceanLevel , value);
_manualOceanLevel = ClampOcean(_manualOceanLevel);
_currentOceanLevel = ClampOcean(_currentOceanLevel);
SaveConfig();
player.Reply($"{GetPrefix(player.Id)} {Msg("MaxSet" , player.Id , _config.MaximumTideLevel)}");
ApplyCurrentMode(true);
return;
}
case "step":
{
float value;
if (args.Length < 2 || !float.TryParse(args[1] , out value))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("InvalidValue" , player.Id)}");
return;
}
_config.IncrementAmountPerTick = Mathf.Max(0.0001f , value);
SaveConfig();
player.Reply($"{GetPrefix(player.Id)} {Msg("StepSet" , player.Id , _config.IncrementAmountPerTick)}");
return;
}
case "interval":
{
float value;
if (args.Length < 2 || !float.TryParse(args[1] , out value))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("InvalidValue" , player.Id)}");
return;
}
_config.UpdateIntervalSeconds = Mathf.Max(0.05f , value);
SaveConfig();
RestartUpdater();
player.Reply($"{GetPrefix(player.Id)} {Msg("IntervalSet" , player.Id , _config.UpdateIntervalSeconds)}");
return;
}
case "hold":
{
float value;
if (args.Length < 2 || !float.TryParse(args[1] , out value))
{
player.Reply($"{GetPrefix(player.Id)} {Msg("InvalidValue" , player.Id)}");
return;
}
_config.TimeBetweenTidesSeconds = Mathf.Max(0f , value);
SaveConfig();
player.Reply($"{GetPrefix(player.Id)} {Msg("HoldSet" , player.Id , _config.TimeBetweenTidesSeconds)}");
return;
}
case "rising":
_rising = true;
_holdUntilRealtime = 0f;
SetTideState(TideState.Rising);
player.Reply($"{GetPrefix(player.Id)} {Msg("DirectionRising" , player.Id)}");
ApplyCurrentMode(true);
return;
case "falling":
_rising = false;
_holdUntilRealtime = 0f;
SetTideState(TideState.Falling);
player.Reply($"{GetPrefix(player.Id)} {Msg("DirectionFalling" , player.Id)}");
ApplyCurrentMode(true);
return;
default:
player.Reply($"{GetPrefix(player.Id)} {Msg("Usage" , player.Id)}");
return;
}
}
#endregion
#region Core
private void StartUpdater()
{
DestroyUpdater();
_updateTimer = timer.Every(_config.UpdateIntervalSeconds , () =>
{
if (!_initialized || !_config.TidesEnabled)
return;
ApplyCurrentMode();
});
ApplyCurrentMode(true);
}
private void RestartUpdater()
{
if (!_initialized)
return;
StartUpdater();
}
private void DestroyUpdater()
{
if (_updateTimer == null)
return;
_updateTimer.Destroy();
_updateTimer = null;
}
private void ApplyCurrentMode(bool force = false)
{
if (_manualOverrideActive)
{
ApplyOceanLevel(_manualOceanLevel , force);
return;
}
if (_paused)
return;
TickIncrementalTide(force);
}
private void TickIncrementalTide(bool force = false)
{
float now = UnityEngine.Time.realtimeSinceStartup;
if (!force && _holdUntilRealtime > now)
{
if (_currentOceanLevel >= _config.MaximumTideLevel)
SetTideState(TideState.HighTide);
else if (_currentOceanLevel <= MinOceanLevel)
SetTideState(TideState.LowTide);
if (_config.Debug)
Puts($"Holding tide. Remaining={_holdUntilRealtime - now:0.###}s State={_tideState}");
return;
}
float previousLevel = _currentOceanLevel;
if (!force)
{
if (_rising)
{
SetTideState(TideState.Rising);
_currentOceanLevel += _config.IncrementAmountPerTick;
}
else
{
SetTideState(TideState.Falling);
_currentOceanLevel -= _config.IncrementAmountPerTick;
}
}
if (_currentOceanLevel >= _config.MaximumTideLevel)
{
_currentOceanLevel = _config.MaximumTideLevel;
if (_rising)
{
SetTideState(TideState.HighTide);
if (_config.SendTideMilestones)
SendChatAnnouncement("ReachedHigh" , _currentOceanLevel);
_rising = false;
_holdUntilRealtime = now + _config.TimeBetweenTidesSeconds;
}
}
else if (_currentOceanLevel <= MinOceanLevel)
{
_currentOceanLevel = MinOceanLevel;
if (!_rising)
{
SetTideState(TideState.LowTide);
if (_config.SendTideMilestones)
SendChatAnnouncement("ReachedLow" , _currentOceanLevel);
_rising = true;
_holdUntilRealtime = now + _config.TimeBetweenTidesSeconds;
}
}
_currentOceanLevel = ClampOcean(_currentOceanLevel);
if (_config.Debug && !force)
Puts($"Tick tide: previous={previousLevel:0.###}, current={_currentOceanLevel:0.###}, state={_tideState}, holdUntil={_holdUntilRealtime:0.###}");
ApplyOceanLevel(_currentOceanLevel , force);
}
private void ApplyOceanLevel(float level , bool force = false)
{
level = ClampOcean(level);
if (!force && Mathf.Abs(level - _lastAppliedOceanLevel) < ChangeThreshold)
return;
if (!SetOceanLevel(level))
return;
_lastAppliedOceanLevel = level;
}
private bool SetOceanLevel(float level)
{
try
{
level = ClampOcean(level);
ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet() , "oceanlevel" , level);
if (WaterSystem.Instance != null)
WaterSystem.OceanLevel = level;
if (_config.Debug)
Puts($"Applied ocean level: {level:0.###}");
return true;
}
catch (Exception ex)
{
PrintError($"Failed to apply ocean level {level:0.###}: {ex}");
return false;
}
}
private float GetCurrentOceanLevel()
{
return Mathf.Max(MinOceanLevel , ConVar.Env.oceanlevel);
}
private float ClampOcean(float level)
{
return Mathf.Clamp(level , MinOceanLevel , _config.MaximumTideLevel);
}
private bool IsHolding()
{
return _holdUntilRealtime > UnityEngine.Time.realtimeSinceStartup;
}
private void SetTideState(TideState newState , bool force = false)
{
if (!force && _tideState == newState)
return;
TideState oldState = _tideState;
_tideState = newState;
Interface.CallHook("OnFloodTideStateChanged" , oldState.ToString() , newState.ToString() , _currentOceanLevel);
if (_config.Debug)
Puts($"Tide state changed: {oldState} -> {newState} at {_currentOceanLevel:0.###}");
}
private int GetTicksBetween(float fromLevel , float toLevel)
{
float distance = Mathf.Abs(toLevel - fromLevel);
if (distance <= 0f)
return 0;
if (_config.IncrementAmountPerTick <= 0f)
return 0;
return Mathf.CeilToInt(distance / _config.IncrementAmountPerTick);
}
private float GetTravelSeconds(float fromLevel , float toLevel)
{
return GetTicksBetween(fromLevel , toLevel) * _config.UpdateIntervalSeconds;
}
private float GetHoldRemainingSeconds()
{
float remaining = _holdUntilRealtime - UnityEngine.Time.realtimeSinceStartup;
return Mathf.Max(0f , remaining);
}
private float GetTimeUntilNextStateChangeInternal()
{
switch (_tideState)
{
case TideState.Rising:
return GetTravelSeconds(_currentOceanLevel , _config.MaximumTideLevel);
case TideState.HighTide:
return GetHoldRemainingSeconds();
case TideState.Falling:
return GetTravelSeconds(_currentOceanLevel , MinOceanLevel);
case TideState.LowTide:
return GetHoldRemainingSeconds();
default:
return 0f;
}
}
private float GetTimeUntilRisingInternal()
{
switch (_tideState)
{
case TideState.Rising:
return 0f;
case TideState.HighTide:
return GetHoldRemainingSeconds() + GetTravelSeconds(_config.MaximumTideLevel , MinOceanLevel) + _config.TimeBetweenTidesSeconds;
case TideState.Falling:
return GetTravelSeconds(_currentOceanLevel , MinOceanLevel) + _config.TimeBetweenTidesSeconds;
case TideState.LowTide:
return GetHoldRemainingSeconds();
default:
return 0f;
}
}
private float GetTimeUntilFallingInternal()
{
switch (_tideState)
{
case TideState.Rising:
return GetTravelSeconds(_currentOceanLevel , _config.MaximumTideLevel) + _config.TimeBetweenTidesSeconds;
case TideState.HighTide:
return GetHoldRemainingSeconds();
case TideState.Falling:
return 0f;
case TideState.LowTide:
return GetHoldRemainingSeconds() + GetTravelSeconds(MinOceanLevel , _config.MaximumTideLevel) + _config.TimeBetweenTidesSeconds;
default:
return 0f;
}
}
private float GetTimeUntilHighTideInternal()
{
switch (_tideState)
{
case TideState.Rising:
return GetTravelSeconds(_currentOceanLevel , _config.MaximumTideLevel);
case TideState.HighTide:
return 0f;
case TideState.Falling:
return GetTravelSeconds(_currentOceanLevel , MinOceanLevel) + _config.TimeBetweenTidesSeconds + GetTravelSeconds(MinOceanLevel , _config.MaximumTideLevel);
case TideState.LowTide:
return GetHoldRemainingSeconds() + GetTravelSeconds(MinOceanLevel , _config.MaximumTideLevel);
default:
return 0f;
}
}
private float GetTimeUntilLowTideInternal()
{
switch (_tideState)
{
case TideState.Rising:
return GetTravelSeconds(_currentOceanLevel , _config.MaximumTideLevel) + _config.TimeBetweenTidesSeconds + GetTravelSeconds(_config.MaximumTideLevel , MinOceanLevel);
case TideState.HighTide:
return GetHoldRemainingSeconds() + GetTravelSeconds(_config.MaximumTideLevel , MinOceanLevel);
case TideState.Falling:
return GetTravelSeconds(_currentOceanLevel , MinOceanLevel);
case TideState.LowTide:
return 0f;
default:
return 0f;
}
}
#endregion
#region API
private object GetTideState() => _tideState.ToString();
private object GetTideStateIndex() => (int)_tideState;
private object GetCurrentTideLevel() => GetCurrentOceanLevel();
private object TideIsRising() => _tideState == TideState.Rising;
private object TideIsFalling() => _tideState == TideState.Falling;
private object TideIsHigh() => _tideState == TideState.HighTide;
private object TideIsLow() => _tideState == TideState.LowTide;
private object GetTimeUntilNextStateChange() => GetTimeUntilNextStateChangeInternal();
private object GetTimeUntilRising() => GetTimeUntilRisingInternal();
private object GetTimeUntilFalling() => GetTimeUntilFallingInternal();
private object GetTimeUntilHighTide() => GetTimeUntilHighTideInternal();
private object GetTimeUntilLowTide() => GetTimeUntilLowTideInternal();
private object GetHoldRemaining() => GetHoldRemainingSeconds();
#endregion
#region Messaging
private void SendChatAnnouncement(string key , params object[] args)
{
var players = BasePlayer.activePlayerList;
if (players == null || players.Count == 0)
return;
foreach (BasePlayer basePlayer in players)
{
if (basePlayer == null || !basePlayer.IsConnected)
continue;
basePlayer.ChatMessage($"{GetPrefix(basePlayer.UserIDString)} {Msg(key , basePlayer.UserIDString , args)}");
}
}
#endregion
}
}