C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("DayNightWeatherController", "SeesAll", "0.7.0")]
[Description("Controls day/night cycles and weather with smart runtime strategies.")]
public class DayNightWeatherController : RustPlugin
{
private PluginConfig config;
private RuntimeStrategy runtimeStrategy = RuntimeStrategy.TrueWorldControl;
private Timer worldControlTimer;
private Timer weatherVerifyTimer;
private bool originalTimeStateCaptured;
private bool originalProgressTime;
private bool originalUseTimeCurve;
private float originalDayLengthInMinutes;
private CyclePhase currentPhase = CyclePhase.Unknown;
private DateTime phaseStartedUtc;
private bool observedFirstPhaseBoundary;
private double lastCompletedDaySeconds = -1d;
private double lastCompletedNightSeconds = -1d;
private const string DefaultAdminOverridePermission = "daynightweathercontroller.adminoverride";
private const float WorldControlIntervalSeconds = 1f;
private const float WeatherVerifyIntervalSeconds = 180f;
private enum RuntimeStrategy
{
TrueWorldControl,
NoControl
}
private enum CyclePhase
{
Unknown,
Day,
Night
}
private class PluginConfig
{
[JsonProperty(PropertyName = "EnvironmentControl")]
public EnvironmentControlSettings EnvironmentControl = new EnvironmentControlSettings();
[JsonProperty(PropertyName = "TimeControl")]
public TimeControlSettings TimeControl = new TimeControlSettings();
[JsonProperty(PropertyName = "WeatherControl")]
public WeatherControlSettings WeatherControl = new WeatherControlSettings();
[JsonProperty(PropertyName = "Permissions")]
public PermissionSettings Permissions = new PermissionSettings();
}
private class EnvironmentControlSettings
{
[JsonProperty(PropertyName = "Enabled")]
public bool Enabled = true;
}
private class TimeControlSettings
{
[JsonProperty(PropertyName = "Mode (1=DAY, 2=NIGHT, 3=CUSTOM, 4=VANILLA)")]
public int Mode = 1;
[JsonProperty(PropertyName = "DayLengthMinutes (Custom Only)")]
public int DayLengthMinutes = 55;
[JsonProperty(PropertyName = "NightLengthMinutes (Custom Only)")]
public int NightLengthMinutes = 5;
[JsonProperty(PropertyName = "NightStartsAtHour (Custom Only, includes dusk)")]
public float NightStartsAtHour = 18.5f;
[JsonProperty(PropertyName = "DayStartsAtHour (Custom Only, includes dawn)")]
public float DayStartsAtHour = 8f;
[JsonProperty(PropertyName = "LockedHour (DAY/NIGHT Only, Recommended Day=12.0, Night=0.0)")]
public float LockedHour = 12f;
}
private class WeatherControlSettings
{
[JsonProperty(PropertyName = "Mode (Clear, Overcast, RainHeavy, RainMild, Storm, Fog, Vanilla)")]
public string Mode = "Clear";
}
private class PermissionSettings
{
[JsonProperty(PropertyName = "UseAdminOverridePermission")]
public string UseAdminOverridePermission = DefaultAdminOverridePermission;
}
protected override void LoadDefaultConfig()
{
config = CreateDefaultConfig();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
var raw = Config.ReadObject<Dictionary<string, object>>();
config = MigrateConfig(raw);
SaveConfig();
}
catch (Exception ex)
{
PrintWarning($"Failed to load config, regenerating default config. Reason: {ex.Message}");
config = CreateDefaultConfig();
SaveConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config, true);
private PluginConfig CreateDefaultConfig()
{
return new PluginConfig();
}
private PluginConfig MigrateConfig(Dictionary<string, object> raw)
{
var migrated = CreateDefaultConfig();
if (raw == null)
return migrated;
var environment = GetMap(raw, "EnvironmentControl");
if (environment != null)
migrated.EnvironmentControl.Enabled = GetBool(environment, "Enabled", migrated.EnvironmentControl.Enabled);
var time = GetMap(raw, "TimeControl");
if (time != null)
{
migrated.TimeControl.Mode =
GetInt(time, "Mode (1=DAY, 2=NIGHT, 3=CUSTOM, 4=VANILLA)",
GetInt(time, "Mode", migrated.TimeControl.Mode));
migrated.TimeControl.DayLengthMinutes =
GetInt(time, "DayLengthMinutes (Custom Only)",
GetInt(time, "DayLengthMinutes", migrated.TimeControl.DayLengthMinutes));
migrated.TimeControl.NightLengthMinutes =
GetInt(time, "NightLengthMinutes (Custom Only)",
GetInt(time, "NightLengthMinutes", migrated.TimeControl.NightLengthMinutes));
migrated.TimeControl.NightStartsAtHour =
GetFloat(time, "NightStartsAtHour (Custom Only, includes dusk)",
GetFloat(time, "NightStartsAtHour", migrated.TimeControl.NightStartsAtHour));
migrated.TimeControl.DayStartsAtHour =
GetFloat(time, "DayStartsAtHour (Custom Only, includes dawn)",
GetFloat(time, "DayStartsAtHour", migrated.TimeControl.DayStartsAtHour));
migrated.TimeControl.LockedHour =
GetFloat(time, "LockedHour (DAY/NIGHT Only, Recommended Day=12.0, Night=0.0)",
GetFloat(time, "LockedHour", migrated.TimeControl.LockedHour));
}
var weather = GetMap(raw, "WeatherControl");
if (weather != null)
{
migrated.WeatherControl.Mode =
GetString(weather, "Mode (Clear, Overcast, RainHeavy, RainMild, Storm, Fog, Vanilla)",
GetString(weather, "Mode (Clear, Overcast, Rain, Storm, Fog, Vanilla)",
GetString(weather, "Mode (Clear, Rain, Storm, Fog, Overcast, Vanilla)",
GetString(weather, "Mode", migrated.WeatherControl.Mode))));
}
var permissionsMap = GetMap(raw, "Permissions");
if (permissionsMap != null)
{
migrated.Permissions.UseAdminOverridePermission =
GetString(permissionsMap, "UseAdminOverridePermission", migrated.Permissions.UseAdminOverridePermission);
}
NormalizeConfig(migrated);
return migrated;
}
private void NormalizeConfig(PluginConfig cfg)
{
if (cfg.TimeControl.Mode < 1 || cfg.TimeControl.Mode > 4)
cfg.TimeControl.Mode = 1;
if (cfg.TimeControl.DayLengthMinutes < 1)
cfg.TimeControl.DayLengthMinutes = 1;
if (cfg.TimeControl.NightLengthMinutes < 1)
cfg.TimeControl.NightLengthMinutes = 1;
cfg.TimeControl.NightStartsAtHour = NormalizeHour(cfg.TimeControl.NightStartsAtHour);
cfg.TimeControl.DayStartsAtHour = NormalizeHour(cfg.TimeControl.DayStartsAtHour);
if (Math.Abs(cfg.TimeControl.NightStartsAtHour - cfg.TimeControl.DayStartsAtHour) < 0.01f)
{
cfg.TimeControl.NightStartsAtHour = 18.5f;
cfg.TimeControl.DayStartsAtHour = 8f;
}
if (cfg.TimeControl.LockedHour < 0f)
cfg.TimeControl.LockedHour = 0f;
if (cfg.TimeControl.LockedHour > 24f)
cfg.TimeControl.LockedHour = 24f;
cfg.WeatherControl.Mode = NormalizeWeatherMode(cfg.WeatherControl.Mode);
if (string.IsNullOrWhiteSpace(cfg.Permissions.UseAdminOverridePermission))
cfg.Permissions.UseAdminOverridePermission = DefaultAdminOverridePermission;
}
private string NormalizeWeatherMode(string input)
{
if (string.IsNullOrWhiteSpace(input))
return "Clear";
string mode = input.Trim();
if (mode.Equals("clear", StringComparison.OrdinalIgnoreCase)) return "Clear";
if (mode.Equals("overcast", StringComparison.OrdinalIgnoreCase)) return "Overcast";
if (mode.Equals("rain", StringComparison.OrdinalIgnoreCase)) return "RainHeavy";
if (mode.Equals("rainheavy", StringComparison.OrdinalIgnoreCase)) return "RainHeavy";
if (mode.Equals("rainmild", StringComparison.OrdinalIgnoreCase)) return "RainMild";
if (mode.Equals("storm", StringComparison.OrdinalIgnoreCase)) return "Storm";
if (mode.Equals("fog", StringComparison.OrdinalIgnoreCase)) return "Fog";
if (mode.Equals("vanilla", StringComparison.OrdinalIgnoreCase)) return "Vanilla";
PrintWarning($"Unknown weather mode '{input}', defaulting to Clear.");
return "Clear";
}
private float NormalizeHour(float hour)
{
hour %= 24f;
return hour < 0f ? hour + 24f : hour;
}
private float ForwardHourSpan(float fromHour, float toHour)
{
float span = NormalizeHour(toHour) - NormalizeHour(fromHour);
return span <= 0f ? span + 24f : span;
}
private Dictionary<string, object> GetMap(Dictionary<string, object> root, string key)
{
object value;
if (!root.TryGetValue(key, out value) || value == null)
return null;
var dict = value as Dictionary<string, object>;
if (dict != null)
return dict;
var jObject = value as Newtonsoft.Json.Linq.JObject;
return jObject != null ? jObject.ToObject<Dictionary<string, object>>() : null;
}
private bool GetBool(Dictionary<string, object> map, string key, bool fallback)
{
object value;
if (!map.TryGetValue(key, out value) || value == null)
return fallback;
try { return Convert.ToBoolean(value); }
catch { return fallback; }
}
private int GetInt(Dictionary<string, object> map, string key, int fallback)
{
object value;
if (!map.TryGetValue(key, out value) || value == null)
return fallback;
try { return Convert.ToInt32(value); }
catch { return fallback; }
}
private float GetFloat(Dictionary<string, object> map, string key, float fallback)
{
object value;
if (!map.TryGetValue(key, out value) || value == null)
return fallback;
try { return Convert.ToSingle(value); }
catch { return fallback; }
}
private string GetString(Dictionary<string, object> map, string key, string fallback)
{
object value;
if (!map.TryGetValue(key, out value) || value == null)
return fallback;
return Convert.ToString(value);
}
private string GetTimeModeName(int mode)
{
switch (mode)
{
case 1: return "DAY";
case 2: return "NIGHT";
case 3: return "CUSTOM";
case 4: return "VANILLA";
default: return $"UNKNOWN({mode})";
}
}
private bool IsForcedWeather()
{
return !string.Equals(config.WeatherControl.Mode, "Vanilla", StringComparison.OrdinalIgnoreCase);
}
private bool HasValidSky()
{
return TOD_Sky.Instance != null && TOD_Sky.Instance.Components != null && TOD_Sky.Instance.Components.Time != null;
}
private void Init()
{
config = config ?? CreateDefaultConfig();
NormalizeConfig(config);
RegisterConfiguredPermission();
}
private void OnServerInitialized()
{
CaptureOriginalTimeState();
ResolveRuntimeStrategy();
if (runtimeStrategy == RuntimeStrategy.TrueWorldControl)
{
InitializePhaseTracking();
ApplyWorldControlState();
StartWorldControlTimer();
}
if (IsForcedWeather())
{
ApplyConfiguredWeather(true);
StartWeatherVerifyTimer();
}
LogRuntimeSummary();
}
private void Unload()
{
worldControlTimer?.Destroy();
weatherVerifyTimer?.Destroy();
RestoreOriginalTimeState();
if (IsForcedWeather())
ConsoleSystem.Run(ConsoleSystem.Option.Server, "weather.reset");
}
private void ResolveRuntimeStrategy()
{
worldControlTimer?.Destroy();
if (!config.EnvironmentControl.Enabled)
{
runtimeStrategy = RuntimeStrategy.NoControl;
return;
}
if (config.TimeControl.Mode == 4)
{
runtimeStrategy = RuntimeStrategy.NoControl;
return;
}
runtimeStrategy = RuntimeStrategy.TrueWorldControl;
}
private void LogRuntimeSummary()
{
Puts("[DayNightWeatherController]");
Puts($"Strategy: {runtimeStrategy}");
Puts($"Time Mode: {GetTimeModeName(config.TimeControl.Mode)}");
Puts($"Weather Mode: {config.WeatherControl.Mode}");
if (config.TimeControl.Mode == 3)
{
Puts($"Visible Night Window: {config.TimeControl.NightStartsAtHour:F2} to {config.TimeControl.DayStartsAtHour:F2}");
Puts($"Configured Durations: Day {config.TimeControl.DayLengthMinutes}m, Night {config.TimeControl.NightLengthMinutes}m");
}
}
private void StartWorldControlTimer()
{
worldControlTimer?.Destroy();
worldControlTimer = timer.Every(WorldControlIntervalSeconds, ApplyWorldControlState);
}
private void ApplyWorldControlState()
{
if (runtimeStrategy != RuntimeStrategy.TrueWorldControl || !HasValidSky())
return;
var timeComponent = TOD_Sky.Instance.Components.Time;
if (config.TimeControl.Mode == 4)
return;
timeComponent.UseTimeCurve = false;
switch (config.TimeControl.Mode)
{
case 1:
timeComponent.ProgressTime = false;
TOD_Sky.Instance.Cycle.Hour = config.TimeControl.LockedHour;
ConVar.Env.time = config.TimeControl.LockedHour;
break;
case 2:
timeComponent.ProgressTime = false;
TOD_Sky.Instance.Cycle.Hour = 0f;
ConVar.Env.time = 0f;
break;
case 3:
ApplyCustomCycleState(timeComponent);
break;
}
}
private void ApplyCustomCycleState(TOD_Time timeComponent)
{
timeComponent.ProgressTime = true;
float currentHour = TOD_Sky.Instance.Cycle.Hour;
CyclePhase phase = IsConfiguredNight(currentHour) ? CyclePhase.Night : CyclePhase.Day;
UpdatePhaseTracking(phase);
float nightSpan = ForwardHourSpan(config.TimeControl.NightStartsAtHour, config.TimeControl.DayStartsAtHour);
float daySpan = Math.Max(0.1f, 24f - nightSpan);
if (phase == CyclePhase.Day)
{
timeComponent.DayLengthInMinutes = config.TimeControl.DayLengthMinutes * (24f / daySpan);
}
else
{
timeComponent.DayLengthInMinutes = config.TimeControl.NightLengthMinutes * (24f / nightSpan);
}
}
private bool IsConfiguredNight(float hour)
{
float start = config.TimeControl.NightStartsAtHour;
float end = config.TimeControl.DayStartsAtHour;
hour = NormalizeHour(hour);
return start < end
? hour >= start && hour < end
: hour >= start || hour < end;
}
private void InitializePhaseTracking()
{
if (!HasValidSky() || config.TimeControl.Mode != 3)
return;
currentPhase = IsConfiguredNight(TOD_Sky.Instance.Cycle.Hour) ? CyclePhase.Night : CyclePhase.Day;
phaseStartedUtc = DateTime.UtcNow;
observedFirstPhaseBoundary = false;
}
private void UpdatePhaseTracking(CyclePhase phase)
{
if (phase == currentPhase)
return;
DateTime now = DateTime.UtcNow;
double elapsedSeconds = phaseStartedUtc == default(DateTime)
? 0d
: (now - phaseStartedUtc).TotalSeconds;
if (observedFirstPhaseBoundary && currentPhase != CyclePhase.Unknown)
{
if (currentPhase == CyclePhase.Day)
lastCompletedDaySeconds = elapsedSeconds;
else if (currentPhase == CyclePhase.Night)
lastCompletedNightSeconds = elapsedSeconds;
Puts($"Completed {currentPhase} phase in {FormatDuration(elapsedSeconds)}.");
}
else
{
observedFirstPhaseBoundary = true;
}
currentPhase = phase;
phaseStartedUtc = now;
Puts($"Entered {currentPhase} phase at server hour {TOD_Sky.Instance.Cycle.Hour:F2}.");
}
private void CaptureOriginalTimeState()
{
if (originalTimeStateCaptured || !HasValidSky())
return;
var timeComponent = TOD_Sky.Instance.Components.Time;
originalProgressTime = timeComponent.ProgressTime;
originalUseTimeCurve = timeComponent.UseTimeCurve;
originalDayLengthInMinutes = timeComponent.DayLengthInMinutes;
originalTimeStateCaptured = true;
}
private void RestoreOriginalTimeState()
{
if (!originalTimeStateCaptured || !HasValidSky())
return;
var timeComponent = TOD_Sky.Instance.Components.Time;
timeComponent.ProgressTime = originalProgressTime;
timeComponent.UseTimeCurve = originalUseTimeCurve;
timeComponent.DayLengthInMinutes = originalDayLengthInMinutes;
}
private string FormatDuration(double totalSeconds)
{
if (totalSeconds < 0d)
return "not measured yet";
TimeSpan duration = TimeSpan.FromSeconds(totalSeconds);
return duration.TotalHours >= 1d
? $"{(int)duration.TotalHours}h {duration.Minutes}m {duration.Seconds}s"
: $"{duration.Minutes}m {duration.Seconds}s";
}
private string BuildStatusMessage()
{
float serverHour = HasValidSky() ? TOD_Sky.Instance.Cycle.Hour : -1f;
string phase = currentPhase.ToString();
string elapsed = phaseStartedUtc == default(DateTime)
? "not available"
: FormatDuration((DateTime.UtcNow - phaseStartedUtc).TotalSeconds);
string effectiveCycle = HasValidSky()
? TOD_Sky.Instance.Components.Time.DayLengthInMinutes.ToString("F2") + " minutes per 24 game hours"
: "not available";
return
$"RuntimeStrategy: {runtimeStrategy}\n" +
$"TimeMode: {GetTimeModeName(config.TimeControl.Mode)}\n" +
$"WeatherMode: {config.WeatherControl.Mode}\n" +
$"ServerHour: {serverHour:F2}\n" +
$"CurrentPhase: {phase} (elapsed since observed boundary/load: {elapsed})\n" +
$"VisibleNightWindow: {config.TimeControl.NightStartsAtHour:F2} to {config.TimeControl.DayStartsAtHour:F2}\n" +
$"ConfiguredDurations: Day {config.TimeControl.DayLengthMinutes}m, Night {config.TimeControl.NightLengthMinutes}m\n" +
$"LastCompletedDay: {FormatDuration(lastCompletedDaySeconds)}\n" +
$"LastCompletedNight: {FormatDuration(lastCompletedNightSeconds)}\n" +
$"EffectiveCycleRate: {effectiveCycle}";
}
private void StartWeatherVerifyTimer()
{
weatherVerifyTimer?.Destroy();
weatherVerifyTimer = timer.Every(WeatherVerifyIntervalSeconds, () =>
{
if (IsForcedWeather())
ApplyConfiguredWeather(false);
});
}
private void ApplyConfiguredWeather(bool forceVanillaReset)
{
string mode = config.WeatherControl.Mode ?? "Clear";
if (string.Equals(mode, "Vanilla", StringComparison.OrdinalIgnoreCase))
{
if (forceVanillaReset)
ConsoleSystem.Run(ConsoleSystem.Option.Server, "weather.reset");
return;
}
ConsoleSystem.Run(ConsoleSystem.Option.Server, $"weather.load {mode}");
}
private void RegisterConfiguredPermission()
{
string perm = config != null && !string.IsNullOrWhiteSpace(config.Permissions.UseAdminOverridePermission)
? config.Permissions.UseAdminOverridePermission
: DefaultAdminOverridePermission;
permission.RegisterPermission(perm, this);
}
private bool HasAdminOverrideAccess(BasePlayer player)
{
if (player == null)
return false;
string perm = config.Permissions.UseAdminOverridePermission;
if (string.IsNullOrWhiteSpace(perm))
perm = DefaultAdminOverridePermission;
return permission.UserHasPermission(player.UserIDString, perm);
}
private bool RequireAdminOverrideAccess(BasePlayer player)
{
if (HasAdminOverrideAccess(player))
return true;
SendReply(player, "You do not have permission to use this command.");
return false;
}
[ChatCommand("envstatus")]
private void CmdEnvStatus(BasePlayer player, string command, string[] args)
{
if (!RequireAdminOverrideAccess(player))
return;
SendReply(player, BuildStatusMessage());
}
[ConsoleCommand("dnwc.status")]
private void CmdConsoleStatus(ConsoleSystem.Arg arg)
{
if (arg.Connection != null && arg.Connection.authLevel < 2)
{
arg.ReplyWith("You do not have permission to use this command.");
return;
}
arg.ReplyWith(BuildStatusMessage());
}
}
}