C#:
using System;
using System.Collections.Generic;
using Oxide.Core.Plugins;
using Oxide.Core;
using UnityEngine;
using Rust;
using System.Collections;
using Oxide.Core.Libraries.Covalence;
using Newtonsoft.Json;
using System.Text;
using System.Linq;
namespace Oxide.Plugins {
[Info("Celestial Barrage", "Ftuoil Xelrash", "1.0.25")]
[Description("Create a Celestial Barrage falling from the sky")]
class CelestialBarrage : RustPlugin {
#region Fields
private Timer EventTimer = null;
private List<Timer> RocketTimers = new List<Timer>();
private HashSet<BaseEntity> meteorRockets = new HashSet<BaseEntity>();
// Track our meteor rockets
private List<(Vector3 position, float time)> recentExplosions = new List<(Vector3, float)>();
// Track recent explosion positions for impact detection
private List<MapMarkerGenericRadius> activeMarkers = new List<MapMarkerGenericRadius>();
// Track map markers
private List<VendingMachineMapMarker> activeVendingMarkers = new List<VendingMachineMapMarker>();
// Track vending markers for hover text
private Dictionary<string, float> discordRateLimiter = new Dictionary<string, float>();
// Rate limit Discord messages
private Dictionary<string, int> discordMessageCount = new Dictionary<string, int>();
// Track message count per minute
private Queue<QueuedDiscordMessage> discordMessageQueue = new Queue<QueuedDiscordMessage>();
// Queue for rate limited messages
private Timer discordQueueTimer = null;
private bool wasGrenadeLauncherOverride = false;
// Track if grenade launcher override occurred
#endregion
#region Oxide Hooks
private void OnServerInitialized() {
try {
lang.RegisterMessages(Messages, this);
LoadVariables();
StartEventTimer();
StartDiscordQueueProcessor();
}
catch (System.Exception ex) {
PrintError($"Error during plugin initialization: {ex.Message}");
}
}
private void Unload() {
StopTimer();
discordQueueTimer?.Destroy();
foreach (var t in RocketTimers) t.Destroy();
var objects = UnityEngine.Object.FindObjectsOfType<ItemCarrier>();
if (objects != null) foreach (var gameObj in objects) UnityEngine.Object.Destroy(gameObj);
// Clean up any remaining map markers
CleanupAllMapMarkers();
}
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
float now = Time.realtimeSinceStartup;
// Purge explosion records older than 0.5 seconds
recentExplosions.RemoveAll(e => now - e.time > 0.5f);
// Check live rockets and recent explosion positions
bool nearMeteor = false;
float closestDistance = float.MaxValue;
foreach (var rocket in meteorRockets) {
if (rocket != null && !rocket.IsDestroyed) {
float d = Vector3.Distance(rocket.transform.position, entity.transform.position);
if (d < 50f) {
nearMeteor = true;
if (d < closestDistance) closestDistance = d;
}
}
}
if (!nearMeteor) {
foreach (var explosion in recentExplosions) {
float d = Vector3.Distance(explosion.position, entity.transform.position);
if (d < 50f) {
nearMeteor = true;
if (d < closestDistance) closestDistance = d;
break;
}
}
}
if (configData?.Logging?.LogDebugToConsole == true && (meteorRockets.Count > 0 || recentExplosions.Count > 0)) {
string weaponInfo = info?.WeaponPrefab?.ShortPrefabName ?? "Unknown";
string attackerInfo = info?.Initiator?.ShortPrefabName ?? "Unknown";
Puts($"[DEBUG] Damage event - Weapon: {weaponInfo}, Attacker: {attackerInfo}, Damage: {info?.damageTypes?.Total():F1}, Entity: {entity?.ShortPrefabName}, NearMeteor: {nearMeteor}");
}
if (nearMeteor) {
if (configData?.Logging?.LogDebugToConsole == true) Puts($"[DEBUG] Meteor impact detected - Distance: {closestDistance:F1}m");
LogMeteorImpact(entity, info);
}
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info) {
// Clean up tracking when our rockets explode/die
if (entity != null && meteorRockets.Contains(entity)) {
if (configData?.Logging?.LogDebugToConsole == true) {
Puts($"[DEBUG] Removing tracked meteor projectile: {entity.ShortPrefabName} (Remaining: {meteorRockets.Count - 1})");
}
// Record explosion position so OnEntityTakeDamage can still match impacts after the rocket is gone
recentExplosions.Add((entity.transform.position, Time.realtimeSinceStartup));
meteorRockets.Remove(entity);
}
}
#endregion
#region Functions
private void StartDiscordQueueProcessor() {
// Process Discord queue every 5 seconds
discordQueueTimer = timer.Repeat(5f, 0, ProcessDiscordQueue);
}
private void ProcessDiscordQueue() {
if (!configData.Logging.DiscordRateLimit.EnableRateLimit) {
// If rate limiting is disabled, send all queued messages immediately
while (discordMessageQueue.Count > 0) {
var queuedMessage = discordMessageQueue.Dequeue();
SendDiscordMessageImmediately(queuedMessage);
}
return;
}
// Reset message counts every minute
float currentTime = UnityEngine.Time.realtimeSinceStartup;
var keysToRemove = new List<string>();
foreach (var kvp in discordRateLimiter) {
if (currentTime - kvp.Value > 60f) // Reset after 1 minute
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove) {
discordRateLimiter.Remove(key);
discordMessageCount.Remove(key);
}
// Process queued messages within rate limits
var messagesToProcess = new List<QueuedDiscordMessage>();
while (discordMessageQueue.Count > 0 && messagesToProcess.Count < 3) // Process max 3 at a time
{
messagesToProcess.Add(discordMessageQueue.Dequeue());
}
foreach (var queuedMessage in messagesToProcess) {
if (CanSendDiscordMessage(queuedMessage.MessageType)) {
SendDiscordMessageImmediately(queuedMessage);
}
else {
// Put it back in queue if still rate limited
discordMessageQueue.Enqueue(queuedMessage);
break;
// Stop processing to avoid infinite loop
}
}
}
private bool CanSendDiscordMessage(string messageType) {
if (!configData.Logging.DiscordRateLimit.EnableRateLimit) return true;
float currentTime = UnityEngine.Time.realtimeSinceStartup;
string rateLimitKey = messageType;
// Check if we've hit the per-minute limit
if (discordMessageCount.ContainsKey(rateLimitKey)) {
if (discordMessageCount[rateLimitKey] >= configData.Logging.DiscordRateLimit.MaxImpactsPerMinute) {
return false;
// Hit per-minute limit
}
}
// Check cooldown between messages
if (discordRateLimiter.ContainsKey(rateLimitKey)) {
float timeSinceLastMessage = currentTime - discordRateLimiter[rateLimitKey];
if (timeSinceLastMessage < configData.Logging.DiscordRateLimit.ImpactMessageCooldown) {
return false;
// Still in cooldown
}
}
return true;
}
private void QueueDiscordMessage(string webhookUrl, string message, bool isAdmin, string messageType) {
try {
var queuedMessage = new QueuedDiscordMessage {
WebhookUrl = webhookUrl, Message = message, IsAdmin = isAdmin, MessageType = messageType, QueuedTime = UnityEngine.Time.realtimeSinceStartup
}
;
if (CanSendDiscordMessage(messageType)) {
// Send immediately if not rate limited
SendDiscordMessageImmediately(queuedMessage);
}
else {
// Queue the message
discordMessageQueue.Enqueue(queuedMessage);
// Enhanced logging for queue status
if (configData.Logging.LogDebugToConsole) {
Puts($"[DISCORD QUEUE] Message queued due to rate limiting. Queue size: {discordMessageQueue.Count}");
}
}
}
catch (System.Exception ex) {
PrintError($"Error queuing Discord message: {ex.Message}");
}
}
private void SendDiscordMessageImmediately(QueuedDiscordMessage queuedMessage) {
// Update rate limiting tracking
if (configData.Logging.DiscordRateLimit.EnableRateLimit) {
float currentTime = UnityEngine.Time.realtimeSinceStartup;
string rateLimitKey = queuedMessage.MessageType;
discordRateLimiter[rateLimitKey] = currentTime;
if (!discordMessageCount.ContainsKey(rateLimitKey)) discordMessageCount[rateLimitKey] = 0;
discordMessageCount[rateLimitKey]++;
}
// Calculate queue delay for display
float queueDelay = UnityEngine.Time.realtimeSinceStartup - queuedMessage.QueuedTime;
// Send the message with queue info passed separately for proper embed formatting
if (queueDelay > 5f) {
SendDiscordEmbedWithQueueInfo(queuedMessage.WebhookUrl, queuedMessage.Message, queuedMessage.IsAdmin, queueDelay, discordMessageQueue.Count);
}
else {
SendDiscordEmbedDirect(queuedMessage.WebhookUrl, queuedMessage.Message, queuedMessage.IsAdmin);
}
// Log when processing queued messages
if (queueDelay > 5f && configData.Logging.LogDebugToConsole) {
Puts($"[DISCORD QUEUE] Sent queued message (delayed {queueDelay:F0}s). {discordMessageQueue.Count} messages remaining in queue.");
}
}
private void StartEventTimer() {
try {
if (configData.Options.EnableAutomaticEvents) {
EventTimer = timer.Repeat(configData.Options.EventTimers.EventIntervalMinutes * 60, 0, () => StartRandomOnMap());
}
}
catch (System.Exception ex) {
PrintError($"Error starting event timer: {ex.Message}");
}
}
private void StopTimer() {
if (EventTimer != null) EventTimer.Destroy();
}
private bool CheckPerformanceAndPlayerCount(string eventType = "Event") {
// Check minimum player requirement
if (configData?.Options?.MinimumPlayerCount != null && BasePlayer.activePlayerList.Count < configData.Options.MinimumPlayerCount) {
string reason = "Not enough players online";
string additionalInfo = $"Current Players: {BasePlayer.activePlayerList.Count} / {configData.Options.MinimumPlayerCount} minimum required";
SendSkippedEventDiscord(reason, eventType, additionalInfo);
// Keep console logging
if (configData.Logging.LogDebugToConsole) {
Puts($"CELESTIAL BARRAGE SKIPPED - Not enough players online ({BasePlayer.activePlayerList.Count} < {configData.Options.MinimumPlayerCount}) - {eventType}");
}
return false;
}
// Check performance monitoring
if (configData?.Options?.PerformanceMonitoring?.EnableFPSCheck == true) {
float currentFPS = 1f / UnityEngine.Time.unscaledDeltaTime;
if (currentFPS < configData.Options.PerformanceMonitoring.MinimumFPS) {
string reason = "Server performance below threshold";
string additionalInfo = $"Current FPS: {currentFPS:F1} / {configData.Options.PerformanceMonitoring.MinimumFPS} minimum required";
SendSkippedEventDiscord(reason, eventType, additionalInfo);
// Keep console logging
if (configData.Logging.LogDebugToConsole) {
Puts($"CELESTIAL BARRAGE CANCELLED - Low FPS detected ({currentFPS:F1} < {configData.Options.PerformanceMonitoring.MinimumFPS}) - {eventType}");
}
return false;
}
}
return true;
}
private void StartRandomOnMap(bool skipPlayerCount = false, bool skipFpsCheck = false) {
// Player count check only applies to automatic events, not admin-triggered ones
if (!skipPlayerCount && !CheckPerformanceAndPlayerCount("Automatic Event")) return;
float mapsize = (TerrainMeta.Size.x / 2) - 600f;
float randomX = UnityEngine.Random.Range(-mapsize, mapsize);
float randomY = UnityEngine.Random.Range(-mapsize, mapsize);
Vector3 callAt = new Vector3(randomX, 0f, randomY);
var selectedSetting = GetRandomIntensitySetting();
StartCelestialEvent(callAt, selectedSetting.setting, "Automatic Event", skipFpsCheck);
}
private (ConfigData.Settings setting, string intensity) GetRandomIntensitySetting() {
var w = configData.IntensitySettings.SpawnWeights;
float total = w.MildWeight + w.MediumWeight + w.ExtremeWeight;
if (total <= 0f) return (configData.IntensitySettings.Mild, "Mild");
float roll = UnityEngine.Random.Range(0f, total);
if (roll < w.MildWeight) return (configData.IntensitySettings.Mild, "Mild");
roll -= w.MildWeight;
if (roll < w.MediumWeight) return (configData.IntensitySettings.Medium, "Medium");
return (configData.IntensitySettings.Extreme, "Extreme");
}
private bool StartOnPlayer(string playerName, ConfigData.Settings setting, string eventType, BasePlayer caller = null, bool skipFpsCheck = false) {
BasePlayer player = GetPlayerByName(playerName, caller);
if (player == null) return false;
StartCelestialEvent(player.transform.position, setting, eventType, skipFpsCheck);
return true;
}
private bool StartRandomOnPlayer(string playerName, string eventType, BasePlayer caller = null, bool skipFpsCheck = false) {
BasePlayer player = GetPlayerByName(playerName, caller);
if (player == null) return false;
var randomSetting = GetRandomIntensitySetting();
StartCelestialEvent(player.transform.position, randomSetting.setting, eventType, skipFpsCheck);
return true;
}
private void StartRandomOnPosition(Vector3 position, string eventType, bool skipFpsCheck = false) {
var randomSetting = GetRandomIntensitySetting();
StartCelestialEvent(position, randomSetting.setting, eventType, skipFpsCheck);
}
private void StartBarrage(Vector3 origin, Vector3 direction) => timer.Repeat(configData.BarrageSettings.RocketDelay, configData.BarrageSettings.NumberOfRockets, () => SpreadRocket(origin, direction));
private void StartCelestialEvent(Vector3 origin, ConfigData.Settings setting, string eventType = "Manual", bool skipFpsCheck = false) {
// FPS check applies to automatic events only; admin-triggered events bypass it
if (!skipFpsCheck && configData?.Options?.PerformanceMonitoring?.EnableFPSCheck == true) {
float currentFPS = 1f / UnityEngine.Time.unscaledDeltaTime;
if (currentFPS < configData.Options.PerformanceMonitoring.MinimumFPS) {
LogMessage($"CELESTIAL BARRAGE BLOCKED\nLow FPS detected ({currentFPS:F1} < {configData.Options.PerformanceMonitoring.MinimumFPS})\n{eventType}", "admin");
return;
}
}
float radius = setting.Radius;
int numberOfRockets = configData.Options.EventTimers.UseRandomTimers ? UnityEngine.Random.Range(setting.RocketAmountMin, setting.RocketAmountMax + 1) : setting.RocketAmountMin;
float duration = configData.Options.EventTimers.UseRandomTimers ? UnityEngine.Random.Range(setting.DurationSecondsMin, setting.DurationSecondsMax) : setting.DurationSecondsMin;
bool dropsItems = setting.ItemDropControl.EnableItemDrop;
ItemDrop[] itemDrops = setting.ItemDropControl.ItemsToDrop;
float intervals = duration / numberOfRockets;
// Determine intensity level by comparing settings objects
string intensity = "Unknown";
if (ReferenceEquals(setting, configData.IntensitySettings.Mild)) intensity = "Mild";
else if (ReferenceEquals(setting, configData.IntensitySettings.Medium)) intensity = "Medium";
else if (ReferenceEquals(setting, configData.IntensitySettings.Extreme)) intensity = "Extreme";
// Log celestial barrage start to console
string gridRef = GetGridReference(origin);
Vector3 groundPos = GetGroundPosition(origin);
string teleportCmd = $"teleportpos {groundPos.x:F1} {groundPos.y:F1} {groundPos.z:F1}";
// Start meteor shower event
StartMeteorShowerWithEffects(origin, setting, eventType, intensity, gridRef, teleportCmd, intervals, numberOfRockets, duration, radius);
}
private void StartMeteorShowerWithEffects(Vector3 origin, ConfigData.Settings setting, string eventType, string intensity, string gridRef, string teleportCmd, float intervals, int numberOfRockets, float duration, float radius) {
string startMessage = $"========== METEOR SHOWER STARTED ==========\n" + $"Type: {eventType} ({intensity})\n" + $"Location: ({origin.x:F0}, {origin.z:F0}) Grid: {gridRef}\n" + $"Stats: {numberOfRockets} rockets ({setting.RocketAmountMin}-{setting.RocketAmountMax} range), {FormatDuration(duration)} duration ({FormatDuration(setting.DurationSecondsMin)}-{FormatDuration(setting.DurationSecondsMax)} range), {radius}m radius\n" + $"Teleport: {teleportCmd}\n" + $"==========================================";
// Send enhanced Discord embeds to admin
SendEnhancedDiscordMessage(true, eventType, intensity, gridRef, numberOfRockets, duration, radius, origin, teleportCmd);
// Send public Discord message if enabled
if (configData.Logging.PublicChannel.Enabled && IsValidWebhookUrl(configData.Logging.PublicChannel.PublicWebhookURL)) {
SendPublicDiscordEmbed($"Celestial barrage started at {gridRef}!", intensity, gridRef, true);
}
// Send in-game notifications (independent of Discord)
if (configData.Options.InGamePlayerEventNotifications) {
// Broadcast location info to all players
foreach (var player in BasePlayer.activePlayerList) {
switch (intensity.ToLower()) {
case "mild" : player.ChatMessage($"<color=#32CD32>CELESTIAL BARRAGE (Mild)</color> started at grid <color=#FFFFFF>{gridRef}</color>");
break;
case "extreme" : player.ChatMessage($"<color=#DC143C>CELESTIAL BARRAGE (Extreme)</color> started at grid <color=#FFFFFF>{gridRef}</color>");
break;
case "medium" : default : player.ChatMessage($"<color=#FFD700>CELESTIAL BARRAGE (Medium)</color> started at grid <color=#FFFFFF>{gridRef}</color>");
break;
}
}
}
// Create map marker if enabled
if (configData.Options.VisualEffects.ShowEventMapMarkers) {
CreateMapMarker(origin, intensity, duration);
}
timer.Repeat(intervals, numberOfRockets, () => RandomRocket(origin, radius, setting));
// Schedule the end event hook to fire after all rockets have been spawned + 15 second buffer
timer.Once(duration + 15f, () => {
string endMessage = $"========== METEOR SHOWER ENDED ===========\n" + $"Type: {eventType} ({intensity})\n" + $"Location: ({origin.x:F0}, {origin.z:F0}) Grid: {gridRef}\n" + $"Teleport: {teleportCmd}\n" + $"==========================================";
// Send enhanced Discord end embed to admin
SendEnhancedDiscordMessage(false, eventType, intensity, gridRef, numberOfRockets, duration, radius, origin, teleportCmd);
// Send public Discord end message if enabled
if (configData.Logging.PublicChannel.Enabled && IsValidWebhookUrl(configData.Logging.PublicChannel.PublicWebhookURL)) {
SendPublicDiscordEmbed($"Celestial barrage ended at {gridRef}", intensity, gridRef, false);
}
// Send in-game notifications (independent of Discord)
if (configData.Options.InGamePlayerEventNotifications) {
// Broadcast end to all players
foreach (var player in BasePlayer.activePlayerList) {
player.ChatMessage($"<color=#20B2AA>Celestial barrage ended at grid {gridRef}</color>");
}
}
// Remove map marker if it exists
// Markers auto-remove via timer, no manual cleanup needed
}
);
}
private void SendEnhancedDiscordMessage(bool isStart, string eventType, string intensity, string gridRef, int numberOfRockets, float duration, float radius, Vector3 origin, string teleportCmd) {
// Only send if admin Discord is enabled and event messages are included
if (!configData.Logging.AdminChannel.Enabled || !configData.Logging.AdminChannel.IncludeEventMessages || !IsValidWebhookUrl(configData.Logging.AdminChannel.PrivateAdminWebhookURL)) return;
float currentFPS = 1f / UnityEngine.Time.unscaledDeltaTime;
int playersOnline = BasePlayer.activePlayerList.Count;
var embed = new object();
if (isStart) {
// Enhanced Started Message - WITH PROPER SECTION SPACING
embed = new {
title = GetIntensityIcon(intensity) + " Celestial Barrage Event", description = "🟢 **STARTED**", color = GetIntensityColor(intensity, true), fields = new[] {
new {
name = "⚡ **Event Information**", value = "", inline = false
}
, new {
name = "", value = $"🎯 **Event Type:** `{eventType} ({intensity})`", inline = false
}
, new {
name = "", value = $"📍 **Grid Location:** `{gridRef}`", inline = false
}
, // SPACING BETWEEN SECTIONS
new {
name = "", value = "", inline = false
}
, new {
name = "📊 **Event Statistics**", value = "", inline = false
}
, new {
name = "", value = $"🚀 **Rocket Count:** `{numberOfRockets}`", inline = false
}
, new {
name = "", value = $"⏱️ **Duration:** `{FormatDuration(duration)}`", inline = false
}
, new {
name = "", value = $"📏 **Impact Radius:** `{radius}m`", inline = false
}
, // SPACING BETWEEN SECTIONS
new {
name = "", value = "", inline = false
}
, new {
name = "⚙️ **Server Status**", value = "", inline = false
}
, new {
name = "", value = $"🖥️ **Server FPS:** `{currentFPS:F1}`", inline = false
}
, new {
name = "", value = $"👥 **Players Online:** `{playersOnline}`", inline = false
}
, // SPACING BETWEEN SECTIONS
new {
name = "", value = "", inline = false
}
, new {
name = "🗺️ **Location Data**", value = "", inline = false
}
, new {
name = "", value = $"🔗 **Teleport Command:** `{teleportCmd}`", inline = false
}
}
, timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), footer = new {
text = $"Celestial Barrage v{Version}"
}
}
;
}
else {
// Enhanced Ended Message - WITH PROPER SECTION SPACING
embed = new {
title = "🌟 Celestial Barrage Event", description = "🔴 **COMPLETED**", color = 0x27AE60, // Success green
fields = new[] {
new {
name = "📋 **Event Summary**", value = "", inline = false
}
, new {
name = "", value = $"🎯 **Event Type:** `{eventType} ({intensity})`", inline = false
}
, new {
name = "", value = $"📍 **Grid Location:** `{gridRef}`", inline = false
}
, // SPACING BETWEEN SECTIONS
new {
name = "", value = "", inline = false
}
, new {
name = "🗺️ **Location Data**", value = "", inline = false
}
, new {
name = "", value = $"🔗 **Teleport Command:** `{teleportCmd}`", inline = false
}
}
, timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), footer = new {
text = $"Celestial Barrage v{Version}"
}
}
;
}
var payload = new {
username = "Celestial Barrage Monitor", avatar_url = "https://cdn.discordapp.com/emojis/1234567890123456789.png", embeds = new[] {
embed
}
}
;
string json = JsonConvert.SerializeObject(payload);
var headers = new Dictionary<string, string> {
{
"Content-Type", "application/json"
}
}
;
webrequest.Enqueue(configData.Logging.AdminChannel.PrivateAdminWebhookURL, json, (code, response) => {
if (code != 200 && code != 204) {
PrintError($"Failed to send enhanced Discord embed to Admin webhook. Response code: {code}");
if (configData.Logging.LogDebugToConsole) {
Puts($"Admin Discord webhook error response: {response}");
}
}
}
, this, Core.Libraries.RequestMethod.POST, headers);
}
private string GetIntensityIcon(string intensity) {
switch (intensity.ToLower()) {
case "mild" : return "💫";
// Changed from 🌿 to 💫
case "medium" : return "⚡";
// Electric energy
case "extreme" : return "🔥";
// Intense fire
default : return "☄️";
// Default meteor
}
}
private int GetIntensityColor(string intensity, bool isStart) {
if (isStart) {
switch (intensity.ToLower()) {
case "mild" : return 0x2ECC71;
// Emerald green
case "medium" : return 0xF39C12;
// Orange
case "extreme" : return 0xE74C3C;
// Crimson red
default : return 0x3498DB;
// Blue
}
}
else {
return 0x27AE60;
// Success green for completed events
}
}
private void RandomRocket(Vector3 origin, float radius, ConfigData.Settings setting) {
try {
bool isFireRocket = false;
Vector2 rand = UnityEngine.Random.insideUnitCircle;
Vector3 offset = new Vector3(rand.x * radius, 0, rand.y * radius);
Vector3 direction = (Vector3.up * -2.0f + Vector3.right).normalized;
Vector3 launchPos = origin + offset - direction * 200;
if (RandomRange(1, setting.FireRocketChance) == 1) isFireRocket = true;
BaseEntity rocket = CreateRocket(launchPos, direction, isFireRocket, setting);
// Check if rocket creation failed
if (rocket == null) {
PrintWarning("Failed to create meteor rocket - skipping this rocket spawn");
return;
}
if (setting.ItemDropControl.EnableItemDrop) {
var comp = rocket.gameObject.AddComponent<ItemCarrier>();
comp.SetCarriedItems(setting.ItemDropControl.ItemsToDrop);
comp.SetDropMultiplier(configData.IntensitySettings.ItemDropMultiplier);
}
// If grenade launcher override occurred, spawn bonus original projectile
if (wasGrenadeLauncherOverride) {
timer.Once(UnityEngine.Random.Range(1f, 3f), () => {
// Create random offset position
Vector2 bonusRand = UnityEngine.Random.insideUnitCircle;
Vector3 bonusOffset = new Vector3(bonusRand.x * radius, 0, bonusRand.y * radius);
Vector3 bonusLaunchPos = origin + bonusOffset - direction * 200;
// Spawn another random projectile (easiest approach)
RandomRocket(origin, radius, setting);
}
);
}
}
catch (System.Exception ex) {
PrintError($"Error creating rocket: {ex.Message}");
}
}
private void SpreadRocket(Vector3 origin, Vector3 direction) {
var barrageSpread = configData.BarrageSettings.RocketSpread;
direction = Quaternion.Euler(UnityEngine.Random.Range((float)(-(double)barrageSpread * 0.5), barrageSpread * 0.5f), UnityEngine.Random.Range((float)(-(double)barrageSpread * 0.5), barrageSpread * 0.5f), UnityEngine.Random.Range((float)(-(double)barrageSpread * 0.5), barrageSpread * 0.5f)) * direction;
BaseEntity rocket = CreateRocket(origin, direction, false, configData.IntensitySettings.Medium);
// Check if rocket creation failed
if (rocket == null) {
PrintWarning("Failed to create barrage rocket - skipping this rocket spawn");
return;
}
}
private BaseEntity CreateRocket(Vector3 startPoint, Vector3 direction, bool isFireRocket, ConfigData.Settings setting) {
ItemDefinition projectileItem = null;
wasGrenadeLauncherOverride = false;
// Reset flag
if (isFireRocket) {
// 1% MLRS, 40% fire rocket, 59% catapult incendiary boulder
float roll = UnityEngine.Random.Range(0f, 1f);
if (roll < 0.01f) projectileItem = ItemManager.FindItemDefinition("ammo.rocket.mlrs");
else if (roll < 0.41f) // 0.01 + 0.40 = 0.41
projectileItem = ItemManager.FindItemDefinition("ammo.rocket.fire");
else projectileItem = ItemManager.FindItemDefinition("catapult.ammo.incendiary");
}
else {
// 20% basic, 20% hv, 20% smoke rocket, 40% catapult boulder
float roll = UnityEngine.Random.Range(0f, 1f);
if (roll < 0.2f) projectileItem = ItemManager.FindItemDefinition("ammo.rocket.basic");
else if (roll < 0.4f) projectileItem = ItemManager.FindItemDefinition("ammo.rocket.hv");
else if (roll < 0.6f) projectileItem = ItemManager.FindItemDefinition("ammo.rocket.smoke");
else projectileItem = ItemManager.FindItemDefinition("catapult.ammo.boulder");
}
// 5% chance to override with grenade launcher (50/50 smoke or HE)
if (UnityEngine.Random.Range(0f, 1f) < 0.05f) {
wasGrenadeLauncherOverride = true;
// Set flag
if (UnityEngine.Random.Range(0f, 1f) < 0.5f) projectileItem = ItemManager.FindItemDefinition("ammo.grenadelauncher.smoke");
else projectileItem = ItemManager.FindItemDefinition("ammo.grenadelauncher.he");
}
// VALIDATION: Fallback to basic rocket if item not found
if (projectileItem == null) {
PrintWarning("Failed to find projectile item, falling back to basic rocket");
projectileItem = ItemManager.FindItemDefinition("ammo.rocket.basic");
// If even basic rocket fails, try one more fallback
if (projectileItem == null) {
string reason = "Critical error: Missing rocket ammo items";
string additionalInfo = "Unable to find any rocket projectiles in item definitions";
SendSkippedEventDiscord(reason, "System Check", additionalInfo);
PrintError("Critical: Cannot find any rocket ammo items! Meteor event cancelled.");
return null;
// Return null to indicate failure
}
}
// Debug logging for projectile creation
if (configData?.Logging?.LogDebugToConsole == true) {
Puts($"[DEBUG] Creating projectile: {projectileItem.shortname}");
}
// Validate component exists
ItemModProjectile component = projectileItem.GetComponent<ItemModProjectile>();
if (component == null) {
PrintError($"Critical: {projectileItem.shortname} has no ItemModProjectile component!");
return null;
}
BaseEntity entity = GameManager.server.CreateEntity(component.projectileObject.resourcePath, startPoint, new Quaternion(), true);
TimedExplosive timedExplosive = entity.GetComponent<TimedExplosive>();
ServerProjectile serverProjectile = entity.GetComponent<ServerProjectile>();
serverProjectile.gravityModifier = 0;
serverProjectile.speed = 25;
timedExplosive.timerAmountMin = 300;
timedExplosive.timerAmountMax = 300;
ScaleAllDamage(timedExplosive.damageTypes, setting.DamageMultiplier);
// Add visual effects if enabled (simplified approach)
if (configData.Options.VisualEffects.EnableParticleTrails) {
AddVisualTrail(entity, projectileItem.shortname);
}
serverProjectile.InitializeVelocity(direction.normalized * 25);
entity.Spawn();
// Track this rocket as one of ours
meteorRockets.Add(entity);
// Debug logging for tracking
if (configData?.Logging?.LogDebugToConsole == true) {
Puts($"[DEBUG] Tracking meteor projectile: {entity.ShortPrefabName} (Total tracked: {meteorRockets.Count})");
}
return entity;
}
private void AddVisualTrail(BaseEntity rocket, string projectileType) {
string effectPath = GetTrailEffectForProjectile(projectileType);
// Skip if no effect specified
if (string.IsNullOrEmpty(effectPath)) return;
float baseFrequency = GetTrailFrequencyForProjectile(projectileType);
int iterations = GetTrailDurationForProjectile(projectileType);
// Track spawn count for this rocket's trail
int spawnCount = 0;
// Create recursive function that generates new random delay for EACH effect spawn
Action spawnNextEffect = null;
spawnNextEffect = () => {
// Check if rocket is still valid and we haven't reached max iterations
if (rocket != null && !rocket.IsDestroyed && spawnCount < iterations) {
// Spawn the effect at current rocket position
Effect.server.Run(effectPath, rocket.transform.position, Vector3.up, null, false);
spawnCount++;
// Generate NEW random delay for THIS iteration (unique every time)
// Increased randomization range (±0.5) for more organic, less mechanical effect patterns
float randomizedFrequency = Mathf.Max(0f, baseFrequency + UnityEngine.Random.Range(-0.5f, 0.5f));
// Schedule the next effect spawn with the random delay
timer.Once(randomizedFrequency, spawnNextEffect);
}
}
;
// Start the trail effect sequence
spawnNextEffect();
}
private string GetTrailEffectForProjectile(string projectileType) {
switch (projectileType) {
case "ammo.rocket.fire" : return "assets/bundled/prefabs/fx/gas_explosion_small.prefab";
// Bigger explosion for fire
case "ammo.rocket.hv" : return "assets/content/vehicles/mlrs/effects/pfx_mlrs_backfire.prefab";
case "ammo.rocket.mlrs" : return "assets/content/vehicles/mlrs/effects/pfx_mlrs_backfire.prefab";
case "ammo.rocket.basic" : return "assets/bundled/prefabs/fx/build/promote_toptier.prefab";
case "catapult.ammo.incendiary" : return "assets/bundled/prefabs/fx/fire/fire_v2.prefab";
case "ammo.rocket.smoke" : return "assets/bundled/prefabs/fx/fire/fire_v3.prefab";
case "catapult.ammo.boulder" : return "assets/bundled/prefabs/fx/weapons/landmine/landmine_explosion.prefab";
case "ammo.grenadelauncher.smoke" : return "assets/content/effects/weather/pfx_lightning_medium.prefab";
case "ammo.grenadelauncher.he" : return "assets/content/effects/weather/pfx_lightning_strong.prefab";
default : return "assets/bundled/prefabs/fx/build/promote_toptier.prefab";
// Standard fallback
}
}
private float GetTrailFrequencyForProjectile(string projectileType) {
switch (projectileType) {
case "ammo.rocket.hv" : return 0.95f;
case "ammo.rocket.mlrs" : return 0.95f;
case "ammo.rocket.fire" : return 0.95f;
case "ammo.rocket.basic" : return 0.45f;
case "ammo.grenadelauncher.he" : return 0.95f;
case "catapult.ammo.incendiary" : return 0.25f;
case "ammo.rocket.smoke" : return 0.15f;
case "ammo.grenadelauncher.smoke" : return 0.95f;
case "catapult.ammo.boulder" : return 2.5f;
default : return 0.45f;
// Standard fallback
}
}
private int GetTrailDurationForProjectile(string projectileType) {
switch (projectileType) {
case "ammo.rocket.hv" : return 5;
case "ammo.rocket.mlrs" : return 5;
case "ammo.rocket.fire" : return 5;
case "ammo.rocket.smoke" : return 45;
case "ammo.grenadelauncher.smoke" : return 10;
case "catapult.ammo.boulder" : return 6;
case "catapult.ammo.incendiary" : return 30;
case "ammo.rocket.basic" : return 30;
case "ammo.grenadelauncher.he" : return 10;
default : return 15;
}
}
private void ScaleAllDamage(List<DamageTypeEntry> damageTypes, float scale) {
for (int i = 0; i < damageTypes.Count; i++) {
damageTypes[i].amount *= scale;
}
}
private void LogMeteorImpact(BaseCombatEntity entity, HitInfo info) {
string entityType = "Unknown";
string ownerInfo = "";
bool isPlayerStructure = false;
bool isPlayer = false;
string damageSource = "";
// Determine what weapon/projectile caused the damage
if (info?.WeaponPrefab != null) {
damageSource = info.WeaponPrefab.ShortPrefabName;
}
float totalDamage = info?.damageTypes?.Total() ?? 0f;
bool isCatapultImpact = damageSource.Contains("boulder") || damageSource.Contains("catapult");
bool isGrenadeImpact = damageSource.Contains("40mm") || damageSource.Contains("grenade");
// Enhanced NPC filtering - check for known NPC types first
if (IsNPCEntity(entity)) {
// This is an NPC - don't log it
return;
}
// Determine what was hit
if (entity is BasePlayer) {
var player = entity as BasePlayer;
// Additional check to filter out NPC players (scientists, etc.)
if (player.IsNpc || player.userID < 76561197960265728L) {
// This is an NPC player - don't log it
return;
}
entityType = "Player";
ownerInfo = player.displayName;
isPlayer = true;
// Screen shake effect for the hit player if enabled
if (configData.Options.VisualEffects.EnableScreenShake) {
ApplyScreenShake(player, 1.0f);
}
}
else if (entity.OwnerID != 0) {
// Player-owned structure
isPlayerStructure = true;
var ownerPlayer = BasePlayer.FindByID(entity.OwnerID);
entityType = entity.ShortPrefabName;
ownerInfo = ownerPlayer != null ? ownerPlayer.displayName : $"OwnerID: {entity.OwnerID}";
// Screen shake for nearby players if enabled
if (configData.Options.VisualEffects.EnableScreenShake) {
ApplyScreenShakeToNearbyPlayers(entity.transform.position, 50f, 0.5f);
}
}
else {
// Natural/unowned entity - don't log these
return;
}
// Apply damage threshold — players always pass, structures/deployables must meet minimum
if (!isPlayer && totalDamage < configData.Logging.AdminChannel.ImpactFiltering.MinimumDamageThreshold) return;
// Log and fire hook for players or player structures only
if (isPlayer || isPlayerStructure) {
string damageInfo = $"Damage: {totalDamage:F1}";
Vector3 pos = entity.transform.position;
string teleportCmd = $"teleportpos {pos.x:F1} {pos.y:F1} {pos.z:F1}";
// Enhanced debug logging
if (configData?.Logging?.LogDebugToConsole == true) {
string projectileType = "Unknown";
if (damageSource.Contains("rocket")) projectileType = "Rocket";
else if (isCatapultImpact) projectileType = "Catapult";
else if (isGrenadeImpact) projectileType = "Grenade";
Puts($"[DEBUG] LOGGING IMPACT - Type: {projectileType}, Weapon: {damageSource}, Damage: {totalDamage:F1}");
}
// Console logging for admin monitoring
if (configData.Logging.LogDebugToConsole) {
string consoleMessage = isPlayer ? $"METEOR IMPACT: {entityType} - Player: {ownerInfo} - Weapon: {damageSource} - {damageInfo}" : $"METEOR IMPACT: {entityType} - Owner: {ownerInfo} - Weapon: {damageSource} - {damageInfo}";
Puts(consoleMessage);
}
// Send beautiful Discord embed message
SendMeteorImpactDiscord(entityType, ownerInfo, damageSource, damageInfo, teleportCmd, isPlayer, isPlayerStructure);
// Fire the hook for other plugins
Interface.CallHook("OnCelestialBarrageImpact", entity, info, entityType, ownerInfo);
}
}
private bool IsNPCEntity(BaseCombatEntity entity) {
if (entity == null) return false;
// Check if it's an NPC player first
if (entity is BasePlayer player) {
return player.IsNpc || player.userID < 76561197960265728L;
}
// Check for common NPC entity types by prefab name
string prefabName = entity.ShortPrefabName.ToLower();
// List of known NPC entities that should be filtered
string[] npcPrefabs = {
"scientist", // Scientists
"murderer", // Murderer NPCs
"bandit", // Bandit NPCs
"dweller", // Tunnel dwellers
"scarecrow", // Scarecrows
"zombie", // Zombies (if any)
"bear", // Bears
"wolf", // Wolves
"chicken", // Chickens
"stag", // Deer/Stags
"boar", // Boars
"horse", // Horses
// ... [Code preview truncated due to length. Full source in attached file] ...