C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Siren", "GNGGOD", "2.1.3")]
[Description("A plugin to play a siren sound in Rust")]
public class Siren : RustPlugin
{
#region Fields
private ConfigData configData;
private readonly Dictionary<BasePlayer, IOEntity> AlarmSound = new Dictionary<BasePlayer, IOEntity>();
private readonly List<IOEntity> MonumentAlarms = new List<IOEntity>();
private readonly List<BaseEntity> SpawnedEffectEntities = new List<BaseEntity>();
private List<Timer> effectTimers = new List<Timer>();
private bool isPlaying = false;
private Timer activeTimer;
private string previousCommandName;
private const string PermissionUse = "siren.use";
private const string PermissionAdmin = "siren.admin";
private Timer scheduleCheckTimer;
private HashSet<string> executedServerSchedulesToday = new HashSet<string>();
private HashSet<string> executedGameSchedulesToday = new HashSet<string>();
private const string DataFileName = "Siren_ScheduleData";
private string lastServerDate;
private int lastGameDay = -1;
#endregion
#region Schedule Data
private class ScheduleData
{
public string LastServerDate { get; set; }
public int LastGameDay { get; set; }
public List<string> ExecutedServerSchedules { get; set; } = new List<string>();
public List<string> ExecutedGameSchedules { get; set; } = new List<string>();
}
#endregion
#region Configuration
public class ConfigData
{
public GeneralSettings General { get; set; }
public SirenSettings Siren { get; set; }
public NotificationSettings Notifications { get; set; }
public ServerTimeScheduleSettings ServerTimeSchedules { get; set; }
public GameTimeScheduleSettings GameTimeSchedules { get; set; }
public bool Debug { get; set; }
}
public class GeneralSettings
{
public string Command { get; set; }
}
public class SirenSettings
{
public string SoundPath { get; set; }
public float Duration { get; set; }
public float Height { get; set; }
public bool UseMonumentSirens { get; set; }
public bool RepeatSound { get; set; }
public float RepeatEverySeconds { get; set; }
}
public class NotificationSettings
{
public bool Enabled { get; set; }
public string Message { get; set; }
public bool UseGUIAnnouncements { get; set; }
public string GUIBannerColor { get; set; }
public string GUITextColor { get; set; }
}
public class ServerTimeScheduleSettings
{
public bool Enabled { get; set; }
public float CheckIntervalSeconds { get; set; }
public string TimeFormat { get; set; }
public List<string> Times { get; set; }
}
public class GameTimeScheduleSettings
{
public bool Enabled { get; set; }
public float CheckIntervalSeconds { get; set; }
public string TimeFormat { get; set; }
public List<string> Times { get; set; }
}
protected override void LoadDefaultConfig()
{
PrintWarning("Creating a new configuration file.");
configData = GetDefaultConfig();
SaveConfig();
}
protected override void SaveConfig()
{
Config.WriteObject(configData, true);
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
configData = Config.ReadObject<ConfigData>();
var defaultConfig = GetDefaultConfig();
var configChanged = false;
foreach (var prop in typeof(ConfigData).GetProperties())
{
var currentValue = prop.GetValue(configData);
if (currentValue == null ||
(prop.PropertyType.IsValueType && currentValue.Equals(Activator.CreateInstance(prop.PropertyType))))
{
prop.SetValue(configData, prop.GetValue(defaultConfig));
configChanged = true;
DebugLog($"Added missing config value: {prop.Name}");
}
}
if (configChanged)
{
SaveConfig();
}
}
catch (Exception ex)
{
PrintError($"Error loading configuration: {ex.Message}. Creating a new one.");
configData = GetDefaultConfig();
SaveConfig();
}
}
private ConfigData GetDefaultConfig()
{
return new ConfigData
{
General = new GeneralSettings
{
Command = "siren"
},
Siren = new SirenSettings
{
SoundPath = "assets/prefabs/io/electric/other/alarmsound.prefab",
Duration = 30.0f,
Height = -10.0f,
UseMonumentSirens = false,
RepeatSound = false,
RepeatEverySeconds = 1.0f
},
Notifications = new NotificationSettings
{
Enabled = true,
Message = "The siren has been triggered!",
UseGUIAnnouncements = false,
GUIBannerColor = "Red",
GUITextColor = "White"
},
ServerTimeSchedules = new ServerTimeScheduleSettings
{
Enabled = false,
CheckIntervalSeconds = 20.0f,
TimeFormat = "24hr format HH:mm (e.g., 14:30, 06:00, 22:15)",
Times = new List<string>()
},
GameTimeSchedules = new GameTimeScheduleSettings
{
Enabled = false,
CheckIntervalSeconds = 20.0f,
TimeFormat = "Decimal format HH.XX (e.g., 6.00, 12.50, 18.75)",
Times = new List<string>()
},
Debug = false
};
}
#endregion
#region Localization
private Dictionary<string, string> messages = new Dictionary<string, string>
{
["NoPermission"] = "You don't have permission to use this command.",
["SirenActivated"] = "Siren sound activated! It will play for {0} seconds.",
["SirenDeactivated"] = "Siren sound deactivated.",
["SirenAlreadyActive"] = "The siren is already active. Please wait until it finishes.",
["SirenStatus"] = "Siren status: {0}. Duration: {1}s. Repeat: {2} ({3}s). Mode: {4}.",
["DurationChanged"] = "Siren duration set to {0} seconds.",
["SoundPathChanged"] = "Siren sound path set to: {0}",
["NotificationMessageChanged"] = "Siren notification message set to: {0}",
["NotificationToggled"] = "Siren notification enabled: {0}",
["InvalidDuration"] = "Invalid duration. Please enter a valid number.",
["UsageSetDuration"] = "Usage: siren.setduration <duration>",
["UsageSetSound"] = "Usage: siren.setsound <soundpath>",
["UsageSetNotification"] = "Usage: siren.setnotification <message>",
["UsageToggleNotification"] = "Usage: siren.enablenotification <true|false>",
["InvalidToggle"] = "Invalid input. Please enter 'true' or 'false'.",
["SirenCommandExecuted"] = "Siren sound command executed!",
["CommandHelp"] = "Siren Commands:\n" +
"/siren - Activate the siren\n" +
"siren - Activate the siren\n" +
"siren status - Show the current siren status and settings\n" +
"siren stop - Stop the siren\n" +
"siren.setduration <seconds> - Set siren duration\n" +
"siren.setsound <path> - Set siren sound path\n" +
"siren.setrepeat <true|false> <interval> - Configure effect repeating\n" +
"siren.setnotification <message> - Set notification message\n" +
"siren.enablenotification <true|false> - Toggle notifications\n" +
"siren.setguiannouncements <true|false> [bannerColor] [textColor] - Configure GUIAnnouncements"
};
private string Lang(string key, string id = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, id), args);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(messages, this);
}
#endregion
#region Oxide Hooks
private void Init()
{
LoadConfig();
LoadDefaultMessages();
LoadScheduleData();
permission.RegisterPermission(PermissionUse, this);
permission.RegisterPermission(PermissionAdmin, this);
previousCommandName = configData.General.Command;
}
private void OnServerInitialized()
{
RegisterCommands();
ValidateSirenPrefab();
StartScheduleTimer();
}
private void Unload()
{
if (isPlaying)
{
StopSiren();
}
activeTimer?.Destroy();
StopScheduleTimer();
SaveScheduleData();
UnregisterCommands();
}
#endregion
#region Plugin Hooks
[PluginReference]
private Plugin GUIAnnouncements;
private bool IsGUIAnnouncementsLoaded()
{
return GUIAnnouncements != null && GUIAnnouncements.IsLoaded;
}
// Track entities spawned by effects during siren playback
private void OnEntitySpawned(BaseEntity entity)
{
if (!isPlaying || entity == null) return;
// Skip players and items
if (entity is BasePlayer || entity is DroppedItem || entity is WorldItem) return;
// Only track entities that match our configured siren prefab path
if (string.IsNullOrEmpty(entity.PrefabName) ||
!entity.PrefabName.Equals(configData.Siren.SoundPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
// Track entities spawned by this plugin for cleanup
SpawnedEffectEntities.Add(entity);
DebugLog($"Tracked spawned siren entity: {entity.ShortPrefabName}");
}
#endregion
#region Commands
private void RegisterCommands()
{
UnregisterCommands();
cmd.AddConsoleCommand(configData.General.Command, this, nameof(PlaySirenConsoleCommand));
cmd.AddChatCommand(configData.General.Command, this, nameof(PlaySirenChatCommand));
cmd.AddConsoleCommand("siren.setduration", this, nameof(SetSirenDurationCommand));
cmd.AddConsoleCommand("siren.setsound", this, nameof(SetSirenSoundCommand));
cmd.AddConsoleCommand("siren.setrepeat", this, nameof(SetSirenRepeatCommand));
cmd.AddConsoleCommand("siren.setnotification", this, nameof(SetSirenNotificationCommand));
cmd.AddConsoleCommand("siren.enablenotification", this, nameof(EnableNotificationCommand));
cmd.AddConsoleCommand("siren.setguiannouncements", this, nameof(SetGUIAnnouncementsCommand));
cmd.AddConsoleCommand("siren.help", this, nameof(SirenHelpCommand));
previousCommandName = configData.General.Command;
DebugLog($"Registered commands: {configData.General.Command}, siren.setduration, siren.setsound, siren.setnotification, siren.enablenotification, siren.setguiannouncements, siren.help");
}
private void UnregisterCommands()
{
if (!string.IsNullOrEmpty(previousCommandName))
{
cmd.RemoveConsoleCommand(previousCommandName, this);
cmd.RemoveChatCommand(previousCommandName, this);
DebugLog($"Unregistered command: {previousCommandName}");
}
cmd.RemoveConsoleCommand("siren.setduration", this);
cmd.RemoveConsoleCommand("siren.setsound", this);
cmd.RemoveConsoleCommand("siren.setrepeat", this);
cmd.RemoveConsoleCommand("siren.setnotification", this);
cmd.RemoveConsoleCommand("siren.enablenotification", this);
cmd.RemoveConsoleCommand("siren.setguiannouncements", this);
cmd.RemoveConsoleCommand("siren.help", this);
DebugLog("Unregistered administrative commands");
}
private void PlaySirenConsoleCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionUse))
{
SendReply(arg, Lang("NoPermission", player?.UserIDString));
return;
}
if (arg.GetString(0, string.Empty).Equals("status", StringComparison.OrdinalIgnoreCase))
{
SendReply(arg, GetSirenStatus(player?.UserIDString));
return;
}
if (arg.GetString(0, string.Empty).Equals("stop", StringComparison.OrdinalIgnoreCase))
{
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player?.UserIDString));
return;
}
StopSiren();
SendReply(arg, Lang("SirenDeactivated", player?.UserIDString));
return;
}
if (isPlaying)
{
SendReply(arg, Lang("SirenAlreadyActive", player?.UserIDString));
return;
}
StartSiren();
DebugLog("Siren sound command executed for all players.");
SendReply(arg, Lang("SirenActivated", player?.UserIDString, configData.Siren.Duration));
}
private void PlaySirenChatCommand(BasePlayer player, string command, string[] args)
{
if (!HasPermission(player, PermissionUse))
{
player.ChatMessage(Lang("NoPermission", player.UserIDString));
return;
}
if (args.Length > 0 && args[0].Equals("status", StringComparison.OrdinalIgnoreCase))
{
player.ChatMessage(GetSirenStatus(player.UserIDString));
return;
}
if (args.Length > 0 && args[0].Equals("stop", StringComparison.OrdinalIgnoreCase))
{
if (!HasPermission(player, PermissionAdmin))
{
player.ChatMessage(Lang("NoPermission", player.UserIDString));
return;
}
StopSiren();
player.ChatMessage(Lang("SirenDeactivated", player.UserIDString));
return;
}
if (isPlaying)
{
player.ChatMessage(Lang("SirenAlreadyActive", player.UserIDString));
return;
}
StartSiren();
DebugLog($"Siren sound command executed by {player.displayName} for all players.");
player.ChatMessage(Lang("SirenActivated", player.UserIDString, configData.Siren.Duration));
}
private string GetSirenStatus(string playerId)
{
return Lang("SirenStatus", playerId,
isPlaying ? "active" : "inactive",
configData.Siren.Duration,
configData.Siren.RepeatSound ? "enabled" : "disabled",
configData.Siren.RepeatEverySeconds,
configData.Siren.UseMonumentSirens ? "monuments" : "players");
}
private void SirenHelpCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
SendReply(arg, Lang("CommandHelp", player?.UserIDString));
}
private void SetSirenDurationCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 1)
{
SendReply(arg, Lang("UsageSetDuration", player?.UserIDString));
return;
}
if (!float.TryParse(arg.GetString(0, string.Empty), out float duration))
{
SendReply(arg, Lang("InvalidDuration", player?.UserIDString));
return;
}
configData.Siren.Duration = duration;
SaveConfig();
SendReply(arg, Lang("DurationChanged", player?.UserIDString, duration));
}
private void SetSirenSoundCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 1)
{
SendReply(arg, Lang("UsageSetSound", player?.UserIDString));
return;
}
configData.Siren.SoundPath = arg.GetString(0, string.Empty);
SaveConfig();
SendReply(arg, Lang("SoundPathChanged", player?.UserIDString, configData.Siren.SoundPath));
}
private void SetSirenRepeatCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 2)
{
SendReply(arg, "Usage: siren.setrepeat <true|false> <interval>");
return;
}
if (!bool.TryParse(arg.GetString(0, string.Empty), out bool repeat))
{
SendReply(arg, "Invalid first argument. Please use 'true' or 'false'.");
return;
}
if (!float.TryParse(arg.GetString(1, string.Empty), out float interval) || interval <= 0)
{
SendReply(arg, "Invalid interval. Please provide a positive number.");
return;
}
configData.Siren.RepeatSound = repeat;
configData.Siren.RepeatEverySeconds = interval;
SaveConfig();
SendReply(arg, $"Siren repeat settings updated: Repeat = {repeat}, Interval = {interval}s");
}
private void SetSirenNotificationCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 1)
{
SendReply(arg, Lang("UsageSetNotification", player?.UserIDString));
return;
}
configData.Notifications.Message = string.Join(" ", Enumerable.Range(0, arg.Args.Length).Select(index => arg.GetString(index, string.Empty)));
SaveConfig();
SendReply(arg, Lang("NotificationMessageChanged", player?.UserIDString, configData.Notifications.Message));
}
private void EnableNotificationCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 1)
{
SendReply(arg, Lang("UsageToggleNotification", player?.UserIDString));
return;
}
if (!bool.TryParse(arg.GetString(0, string.Empty), out bool enable))
{
SendReply(arg, Lang("InvalidToggle", player?.UserIDString));
return;
}
configData.Notifications.Enabled = enable;
SaveConfig();
SendReply(arg, Lang("NotificationToggled", player?.UserIDString, enable));
}
#endregion
#region Helper Methods
private bool HasPermission(BasePlayer player, string perm)
{
return player != null && permission.UserHasPermission(player.UserIDString, perm);
}
private void DebugLog(string message)
{
if (configData.Debug)
{
Puts($"[Siren Debug] {message}");
}
}
private void ValidateSirenPrefab()
{
if (string.IsNullOrEmpty(configData.Siren.SoundPath))
{
PrintError("Siren sound path is empty! Please set a valid path.");
return;
}
var prefab = GameManager.server.FindPrefab(configData.Siren.SoundPath);
if (prefab == null)
{
PrintWarning($"Could not find prefab at path: {configData.Siren.SoundPath}");
PrintWarning("The siren might not work correctly. Please check the path in your config.");
PrintWarning("Try one of these common siren prefabs:");
PrintWarning("- assets/prefabs/io/electric/other/alarmsound.prefab");
PrintWarning("- assets/prefabs/misc/easter/painted eggs/effects/gold_open.prefab");
PrintWarning("- assets/prefabs/npc/autoturret/effects/online.prefab");
}
}
#endregion
#region Scheduled Sirens Logic
private void LoadScheduleData()
{
try
{
var data = Interface.Oxide.DataFileSystem.ReadObject<ScheduleData>(DataFileName);
if (data != null)
{
lastServerDate = data.LastServerDate;
lastGameDay = data.LastGameDay;
executedServerSchedulesToday = new HashSet<string>(data.ExecutedServerSchedules ?? new List<string>());
executedGameSchedulesToday = new HashSet<string>(data.ExecutedGameSchedules ?? new List<string>());
DebugLog($"Loaded schedule data: ServerDate={lastServerDate}, GameDay={lastGameDay}");
}
else
{
lastServerDate = DateTime.Now.ToString("yyyy-MM-dd");
lastGameDay = GetCurrentGameDay();
executedServerSchedulesToday = new HashSet<string>();
executedGameSchedulesToday = new HashSet<string>();
}
}
catch
{
lastServerDate = DateTime.Now.ToString("yyyy-MM-dd");
lastGameDay = GetCurrentGameDay();
executedServerSchedulesToday = new HashSet<string>();
executedGameSchedulesToday = new HashSet<string>();
DebugLog("No existing schedule data found, starting fresh.");
}
}
private void SaveScheduleData()
{
var data = new ScheduleData
{
LastServerDate = lastServerDate,
LastGameDay = lastGameDay,
ExecutedServerSchedules = executedServerSchedulesToday.ToList(),
ExecutedGameSchedules = executedGameSchedulesToday.ToList()
};
Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
DebugLog($"Saved schedule data: ServerDate={lastServerDate}, GameDay={lastGameDay}");
}
private void StartScheduleTimer()
{
bool hasServerSchedules = configData.ServerTimeSchedules.Enabled &&
configData.ServerTimeSchedules.Times != null &&
configData.ServerTimeSchedules.Times.Count > 0;
bool hasGameSchedules = configData.GameTimeSchedules.Enabled &&
configData.GameTimeSchedules.Times != null &&
configData.GameTimeSchedules.Times.Count > 0;
if (!hasServerSchedules && !hasGameSchedules)
{
DebugLog("No scheduled sirens configured or enabled.");
return;
}
// Use the smaller check interval of the two
float checkInterval = float.MaxValue;
if (hasServerSchedules)
checkInterval = Math.Min(checkInterval, configData.ServerTimeSchedules.CheckIntervalSeconds);
if (hasGameSchedules)
checkInterval = Math.Min(checkInterval, configData.GameTimeSchedules.CheckIntervalSeconds);
scheduleCheckTimer?.Destroy();
scheduleCheckTimer = timer.Repeat(checkInterval, 0, CheckSchedules);
if (hasServerSchedules)
Puts($"[Siren] Server time schedules enabled. {configData.ServerTimeSchedules.Times.Count} time(s) configured.");
if (hasGameSchedules)
Puts($"[Siren] Game time schedules enabled. {configData.GameTimeSchedules.Times.Count} time(s) configured.");
}
private void StopScheduleTimer()
{
scheduleCheckTimer?.Destroy();
scheduleCheckTimer = null;
}
private void CheckSchedules()
{
// Check server time schedules
if (configData.ServerTimeSchedules.Enabled &&
configData.ServerTimeSchedules.Times != null &&
configData.ServerTimeSchedules.Times.Count > 0)
{
CheckServerTimeSchedules();
}
// Check game time schedules
if (configData.GameTimeSchedules.Enabled &&
configData.GameTimeSchedules.Times != null &&
configData.GameTimeSchedules.Times.Count > 0)
{
CheckGameTimeSchedules();
}
}
private void CheckServerTimeSchedules()
{
// Check for server day reset
string currentServerDate = DateTime.Now.ToString("yyyy-MM-dd");
if (lastServerDate != currentServerDate)
{
Puts($"[Siren] New server day detected. Resetting server time schedules.");
executedServerSchedulesToday.Clear();
lastServerDate = currentServerDate;
SaveScheduleData();
}
DateTime now = DateTime.Now;
float currentHour = now.Hour;
float currentMinute = now.Minute + (now.Second / 60f);
float currentTimeInMinutes = (currentHour * 60) + currentMinute;
DebugLog($"Server time check: {currentHour:00}:{currentMinute:00.0}");
foreach (var scheduleTime in configData.ServerTimeSchedules.Times)
{
if (executedServerSchedulesToday.Contains(scheduleTime))
continue;
if (!TryParseServerTime(scheduleTime, out float scheduleHour, out float scheduleMinute))
{
DebugLog($"Invalid server time format: {scheduleTime} (expected HH:mm)");
continue;
}
float scheduleTimeInMinutes = (scheduleHour * 60) + scheduleMinute;
float timeDifferenceMinutes = currentTimeInMinutes - scheduleTimeInMinutes;
// Grace period: 2x check interval converted to minutes
float gracePeriodMinutes = (configData.ServerTimeSchedules.CheckIntervalSeconds * 2f) / 60f;
DebugLog($"Server schedule '{scheduleTime}': diff={timeDifferenceMinutes:F1}min (grace=0 to {gracePeriodMinutes:F1})");
if (timeDifferenceMinutes >= 0 && timeDifferenceMinutes <= gracePeriodMinutes)
{
Puts($"[Siren] Triggering server time schedule: {scheduleTime}");
executedServerSchedulesToday.Add(scheduleTime);
SaveScheduleData();
TriggerScheduledSiren();
}
}
}
private void CheckGameTimeSchedules()
{
// Check for game day reset
int currentGameDay = GetCurrentGameDay();
if (lastGameDay != currentGameDay)
{
Puts($"[Siren] New game day detected (Day {currentGameDay}). Resetting game time schedules.");
executedGameSchedulesToday.Clear();
lastGameDay = currentGameDay;
SaveScheduleData();
}
float gameHour = TOD_Sky.Instance.Cycle.Hour;
DebugLog($"Game time check: {gameHour:F2}");
foreach (var scheduleTime in configData.GameTimeSchedules.Times)
{
if (executedGameSchedulesToday.Contains(scheduleTime))
continue;
if (!TryParseGameTime(scheduleTime, out float scheduleHour))
{
DebugLog($"Invalid game time format: {scheduleTime} (expected HH.XX like 18.50)");
continue;
}
float timeDifference = gameHour - scheduleHour;
// Grace period: 5 game-time units (since game time is in decimal hours, this is generous)
float gracePeriod = 0.5f; // Half an in-game hour
DebugLog($"Game schedule '{scheduleTime}': current={gameHour:F2}, target={scheduleHour:F2}, diff={timeDifference:F2} (grace=0 to {gracePeriod:F2})");
if (timeDifference >= 0 && timeDifference <= gracePeriod)
{
Puts($"[Siren] Triggering game time schedule: {scheduleTime} (current: {gameHour:F2})");
executedGameSchedulesToday.Add(scheduleTime);
SaveScheduleData();
TriggerScheduledSiren();
}
}
}
private void TriggerScheduledSiren()
{
if (!isPlaying)
{
StartSiren();
}
else
{
DebugLog("Siren already playing, skipping scheduled trigger.");
}
}
private int GetCurrentGameDay()
{
return (int)(TOD_Sky.Instance?.Cycle?.Day ?? 0);
}
private bool TryParseServerTime(string timeStr, out float hour, out float minute)
{
hour = 0;
minute = 0;
if (string.IsNullOrEmpty(timeStr))
return false;
// Server time uses colon format: "HH:mm" like "14:30"
var parts = timeStr.Split(':');
if (parts.Length != 2)
return false;
if (!float.TryParse(parts[0], out hour) || !float.TryParse(parts[1], out minute))
return false;
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
return false;
return true;
}
private bool TryParseGameTime(string timeStr, out float hour)
{
hour = 0;
if (string.IsNullOrEmpty(timeStr))
return false;
// Game time uses decimal format: "HH.XX" like "18.50" (Rust's native format)
if (!float.TryParse(timeStr, out hour))
return false;
if (hour < 0 || hour >= 24)
return false;
return true;
}
#endregion
#region Siren Logic
private void StartSiren()
{
isPlaying = true;
if (configData.Siren.RepeatSound && configData.Siren.RepeatEverySeconds <= 0)
{
PrintWarning("RepeatEverySeconds must be greater than 0. Disabling repeat.");
configData.Siren.RepeatSound = false;
}
if (configData.Notifications.UseGUIAnnouncements && IsGUIAnnouncementsLoaded())
{
DebugLog("Using GUIAnnouncements for notification");
GUIAnnouncements.Call("CreateAnnouncement", configData.Notifications.Message,
configData.Notifications.GUIBannerColor, configData.Notifications.GUITextColor);
}
if (configData.Siren.UseMonumentSirens)
{
StartMonumentSirens();
}
else
{
foreach (var player in BasePlayer.activePlayerList)
{
if (IsEffectPath(configData.Siren.SoundPath))
{
DebugLog($"Using EFFECT path for player {player.displayName}: {configData.Siren.SoundPath}");
PlayEffectForPlayer(player);
}
else
{
DebugLog($"Using ENTITY path for player {player.displayName}: {configData.Siren.SoundPath}");
AddSoundEntityToPlayer(player);
}
}
}
activeTimer?.Destroy();
activeTimer = timer.Once(configData.Siren.Duration, StopSiren);
}
private bool IsEffectPath(string path)
{
return path.Contains("/effects/") ||
path.Contains("/fx/") ||
(path.EndsWith(".prefab") && path.ToLower().Contains("effect"));
}
private void PlayEffectForPlayer(BasePlayer player)
{
Vector3 position = player.transform.position + new Vector3(0f, configData.Siren.Height, 0f);
Effect.server.Run(configData.Siren.SoundPath, position);
DebugLog($"Effect started for player {player.displayName} at {position}.");
if (configData.Siren.RepeatSound)
{
Timer effectTimer = timer.Repeat(configData.Siren.RepeatEverySeconds,
(int)(configData.Siren.Duration / configData.Siren.RepeatEverySeconds), () =>
{
if (!isPlaying || player == null || !player.IsConnected) return;
position = player.transform.position + new Vector3(0f, configData.Siren.Height, 0f);
Effect.server.Run(configData.Siren.SoundPath, position);
});
effectTimers.Add(effectTimer);
}
if (configData.Notifications.Enabled && !(configData.Notifications.UseGUIAnnouncements && IsGUIAnnouncementsLoaded()))
{
SendReply(player, configData.Notifications.Message);
}
}
private void StartMonumentSirens()
{
List<MonumentInfo> monuments = TerrainMeta.Path.Monuments;
DebugLog($"Found {monuments.Count} total monuments on the map");
List<string> excludeKeywords = new List<string>
{
"lake", "canyon", "river", "mountain", "cliff", "beach", "road", "forest", "swamp"
};
int sirenCount = 0;
bool isEffect = IsEffectPath(configData.Siren.SoundPath);
foreach (var monument in monuments)
{
if (monument.name == null)
continue;
string monumentName = monument.name.ToLower();
if (excludeKeywords.Any(keyword => monumentName.Contains(keyword)) ||
monument.Bounds.size.magnitude < 20f)
{
DebugLog($"Skipping monument: {monument.name}");
continue;
}
Vector3 position = monument.transform.position + new Vector3(0f, 15f, 0f);
if (isEffect)
{
Effect.server.Run(configData.Siren.SoundPath, position);
if (configData.Siren.RepeatSound)
{
Timer effectTimer = timer.Repeat(configData.Siren.RepeatEverySeconds,
(int)(configData.Siren.Duration / configData.Siren.RepeatEverySeconds), () =>
{
if (!isPlaying) return;
Effect.server.Run(configData.Siren.SoundPath, position);
});
effectTimers.Add(effectTimer);
}
}
else
{
IOEntity alarm = GameManager.server.CreateEntity(configData.Siren.SoundPath, position) as IOEntity;
if (alarm == null)
{
DebugLog($"Failed to create siren entity for monument {monument.name}.");
continue;
}
alarm.enableSaving = false;
alarm.Spawn();
MonumentAlarms.Add(alarm);
alarm.gameObject.SetActive(true);
alarm.SetFlagLocal(BaseEntity.Flags.On, true);
alarm.UpdateFromInput(400, 0);
alarm.SendNetworkUpdateImmediate();
}
sirenCount++;
DebugLog($"Siren started at monument: {monument.name}");
}
DebugLog($"Successfully placed {sirenCount} sirens at suitable monuments");
if (configData.Notifications.Enabled)
{
foreach (var target in BasePlayer.activePlayerList)
{
SendReply(target, configData.Notifications.Message);
}
}
}
private void StopSiren()
{
DebugLog("Stopping the siren sound for all players.");
int playerAlarmsKilled = 0;
foreach (var entry in AlarmSound.ToList())
{
if (entry.Value != null && !entry.Value.IsDestroyed)
{
entry.Value.Kill();
playerAlarmsKilled++;
}
}
DebugLog($"Cleaned up {playerAlarmsKilled} player alarm entities.");
int monumentAlarmsKilled = 0;
foreach (var alarm in MonumentAlarms)
{
if (alarm != null && !alarm.IsDestroyed)
{
alarm.Kill();
monumentAlarmsKilled++;
}
}
DebugLog($"Cleaned up {monumentAlarmsKilled} monument alarm entities.");
MonumentAlarms.Clear();
int effectEntitiesKilled = 0;
foreach (var entity in SpawnedEffectEntities)
{
if (entity != null && !entity.IsDestroyed)
{
entity.Kill();
effectEntitiesKilled++;
}
}
DebugLog($"Cleaned up {effectEntitiesKilled} spawned effect entities.");
SpawnedEffectEntities.Clear();
int timersDestroyed = 0;
foreach (var effectTimer in effectTimers)
{
if (effectTimer != null)
{
effectTimer.Destroy();
timersDestroyed++;
}
}
DebugLog($"Destroyed {timersDestroyed} effect timers.");
AlarmSound.Clear();
effectTimers.Clear();
isPlaying = false;
activeTimer?.Destroy();
activeTimer = null;
DebugLog("Siren cleanup complete.");
}
private void AddSoundEntityToPlayer(BasePlayer player)
{
foreach (var target in AlarmSound.ToList())
{
if (target.Key == null || target.Key == player) continue;
float distance = Vector3.Distance(player.transform.position, target.Key.transform.position);
if (distance < 20)
{
DebugLog($"Skipping creation of siren entity for player {player.displayName} as one is already nearby.");
return;
}
}
IOEntity alarm = GameManager.server.CreateEntity(configData.Siren.SoundPath,
player.transform.position + new Vector3(0f, configData.Siren.Height, 0f)) as IOEntity;
if (alarm == null)
{
DebugLog($"Failed to create siren entity for player {player.displayName}.");
return;
}
alarm.enableSaving = false;
alarm.Spawn();
// Parent to player so the alarm follows them
alarm.SetParent(player, true, true);
alarm.transform.localPosition = new Vector3(0f, configData.Siren.Height, 0f);
AlarmSound[player] = alarm;
alarm.gameObject.SetActive(true);
alarm.SetFlagLocal(BaseEntity.Flags.On, true);
alarm.UpdateFromInput(400, 0);
alarm.SendNetworkUpdateImmediate();
DebugLog($"Siren sound started for player {player.displayName} (parented, offset: {configData.Siren.Height}).");
if (configData.Notifications.Enabled && !(configData.Notifications.UseGUIAnnouncements && IsGUIAnnouncementsLoaded()))
{
SendReply(player, configData.Notifications.Message);
}
}
private void SetGUIAnnouncementsCommand(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !HasPermission(player, PermissionAdmin))
{
SendReply(arg, Lang("NoPermission", player.UserIDString));
return;
}
if (arg.Args == null || arg.Args.Length < 1)
{
SendReply(arg, "Usage: siren.setguiannouncements <true|false> [bannerColor] [textColor]");
return;
}
if (!bool.TryParse(arg.GetString(0, string.Empty), out bool useGUI))
{
SendReply(arg, "Invalid first argument. Please use 'true' or 'false'.");
return;
}
configData.Notifications.UseGUIAnnouncements = useGUI;
if (arg.Args.Length > 1)
{
configData.Notifications.GUIBannerColor = arg.GetString(1, string.Empty);
}
if (arg.Args.Length > 2)
{
configData.Notifications.GUITextColor = arg.GetString(2, string.Empty);
}
SaveConfig();
SendReply(arg, $"GUIAnnouncements settings updated: Enabled = {useGUI}, BannerColor = {configData.Notifications.GUIBannerColor}, TextColor = {configData.Notifications.GUITextColor}");
}
#endregion
}
}