using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; namespace Oxide.Plugins { [Info("Corrosion Companion", "Corrosion", "1.0.0")] [Description("Connects Rust server to Corrosion admin panel via HTTP API")] public class CorrosionCompanion : RustPlugin { #region Configuration private Configuration config; public class Configuration { [JsonProperty("API Base URL")] public string ApiBaseUrl { get; set; } = "https://api.corrosion.example.com"; [JsonProperty("License Key")] public string LicenseKey { get; set; } = "YOUR_LICENSE_KEY_HERE"; [JsonProperty("Heartbeat Interval (seconds)")] public int HeartbeatInterval { get; set; } = 60; [JsonProperty("Send Player Events")] public bool SendPlayerEvents { get; set; } = true; [JsonProperty("Send Chat Events")] public bool SendChatEvents { get; set; } = false; [JsonProperty("Debug Mode")] public bool DebugMode { get; set; } = false; } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) { throw new Exception("Config is null"); } } catch { PrintWarning("Config file corrupt or missing, generating new one"); LoadDefaultConfig(); } } protected override void LoadDefaultConfig() { config = new Configuration(); SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(config); #endregion #region Lifecycle Hooks private Timer heartbeatTimer; void OnServerInitialized() { Puts("Corrosion Companion initialized"); // Validate configuration if (string.IsNullOrEmpty(config.LicenseKey) || config.LicenseKey == "YOUR_LICENSE_KEY_HERE") { PrintError("License key not configured! Edit oxide/config/CorrosionCompanion.json"); return; } // Send initial check-in CheckIn(); // Start heartbeat timer heartbeatTimer = timer.Every(config.HeartbeatInterval, () => { SendHeartbeat(); }); Puts($"Heartbeat started (every {config.HeartbeatInterval}s)"); } void Unload() { heartbeatTimer?.Destroy(); Puts("Corrosion Companion unloaded"); } #endregion #region Player Event Hooks void OnPlayerConnected(BasePlayer player) { if (!config.SendPlayerEvents) return; var data = new Dictionary { { "license_key", config.LicenseKey }, { "event", "player_connected" }, { "player_id", player.UserIDString }, { "player_name", player.displayName }, { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }; SendApiRequest("/api/plugin/player-event", data, (code, response) => { if (config.DebugMode) { if (code == 200) { Puts($"Player join tracked: {player.displayName} ({player.UserIDString})"); } else { PrintWarning($"Player join tracking failed: HTTP {code}"); } } }); } void OnPlayerDisconnected(BasePlayer player, string reason) { if (!config.SendPlayerEvents) return; var data = new Dictionary { { "license_key", config.LicenseKey }, { "event", "player_disconnected" }, { "player_id", player.UserIDString }, { "player_name", player.displayName }, { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }; SendApiRequest("/api/plugin/player-event", data, (code, response) => { if (config.DebugMode) { if (code == 200) { Puts($"Player leave tracked: {player.displayName} (Reason: {reason})"); } else { PrintWarning($"Player leave tracking failed: HTTP {code}"); } } }); } object OnPlayerChat(BasePlayer player, string message) { if (!config.SendChatEvents) return null; var data = new Dictionary { { "event", "player_chat" }, { "player_id", player.UserIDString }, { "player_name", player.displayName }, { "message", message }, { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }; SendEvent("player_chat", data); return null; // Don't block the message } #endregion #region API Communication private void CheckIn() { var data = new Dictionary { { "license_key", config.LicenseKey }, { "server_name", ConVar.Server.hostname }, { "server_description", ConVar.Server.description }, { "server_url", ConVar.Server.url }, { "max_players", ConVar.Server.maxplayers }, { "world_size", ConVar.Server.worldsize }, { "seed", ConVar.Server.seed }, { "plugin_version", Version.ToString() }, { "server_version", Rust.Protocol.network.ToString() } }; SendApiRequest("/api/plugin/checkin", data, (code, response) => { if (code == 200) { Puts("Check-in successful"); if (config.DebugMode) { Puts($"Response: {response}"); } } else { PrintWarning($"Check-in failed: HTTP {code}"); } }); } private void SendHeartbeat() { var data = new Dictionary { { "license_key", config.LicenseKey }, { "player_count", BasePlayer.activePlayerList.Count }, { "max_players", ConVar.Server.maxplayers }, { "fps", Performance.current.frameRate }, { "entity_count", BaseNetworkable.serverEntities.Count }, { "uptime_seconds", Time.realtimeSinceStartup }, { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }; SendApiRequest("/api/plugin/heartbeat", data, (code, response) => { if (config.DebugMode) { if (code == 200) { Puts($"Heartbeat sent (Players: {BasePlayer.activePlayerList.Count}, FPS: {Performance.current.frameRate:F1})"); } else { PrintWarning($"Heartbeat failed: HTTP {code}"); } } }); } private void SendEvent(string eventType, Dictionary data) { data["license_key"] = config.LicenseKey; SendApiRequest($"/api/plugin/events/{eventType}", data, (code, response) => { if (config.DebugMode && code != 200) { PrintWarning($"Event {eventType} failed: HTTP {code}"); } }); } private void SendApiRequest(string endpoint, Dictionary data, Action callback) { string url = config.ApiBaseUrl.TrimEnd('/') + endpoint; string json = JsonConvert.SerializeObject(data); webrequest.Enqueue(url, json, (code, response) => { callback?.Invoke(code, response ?? ""); }, this, RequestMethod.POST, new Dictionary { { "Content-Type", "application/json" } }); } #endregion #region Console Commands [Command("corrosion.status")] private void StatusCommand(IPlayer player, string command, string[] args) { if (!player.IsAdmin) { player.Reply("You don't have permission to use this command"); return; } player.Reply("=== Corrosion Companion Status ==="); player.Reply($"Version: {Version}"); player.Reply($"License Key: {config.LicenseKey.Substring(0, Math.Min(8, config.LicenseKey.Length))}..."); player.Reply($"API URL: {config.ApiBaseUrl}"); player.Reply($"Heartbeat Interval: {config.HeartbeatInterval}s"); player.Reply($"Player Events: {(config.SendPlayerEvents ? "Enabled" : "Disabled")}"); player.Reply($"Chat Events: {(config.SendChatEvents ? "Enabled" : "Disabled")}"); player.Reply($"Debug Mode: {(config.DebugMode ? "Enabled" : "Disabled")}"); player.Reply($"Active Players: {BasePlayer.activePlayerList.Count}"); } [Command("corrosion.checkin")] private void CheckinCommand(IPlayer player, string command, string[] args) { if (!player.IsAdmin) { player.Reply("You don't have permission to use this command"); return; } player.Reply("Sending check-in to Corrosion API..."); CheckIn(); } [Command("corrosion.heartbeat")] private void HeartbeatCommand(IPlayer player, string command, string[] args) { if (!player.IsAdmin) { player.Reply("You don't have permission to use this command"); return; } player.Reply("Sending heartbeat to Corrosion API..."); SendHeartbeat(); } #endregion } }