using OpenMetaverse; using OpenMetaverse.Assets; using SecondBotEvents.Config; using System; using System.Threading; using System.Timers; using Timer = System.Timers.Timer; namespace SecondBotEvents.Services { public class BotClientService : BotServices { public GridClient client = null; public BasicConfig basicCfg = null; private BotRejoinConfig rejoinCfg; protected bool LogoutExpected = false; protected int consecutiveLoginFailures = 0; protected DateTime lastConnectTime = DateTime.MinValue; protected bool isRegionRestartScheduled = false; public void flagLogoutExpected() { LogoutExpected = true; } public bool getLogoutExpected() { return LogoutExpected; } public bool IsConnected() { if (client == null) { return false; } else if (client.Network == null) { return false; } else if (client.Network.Connected == false) { return false; } else if (client.Network.CurrentSim == null) { return false; } else if (client.Network.CurrentSim.Connected == false) { return false; } return true; } protected bool ExitBot = false; protected Timer AutoRestartLoginTimer; public BotClientService(EventsSecondBot setMaster) : base(setMaster) { basicCfg = new BasicConfig(master.fromEnv, master.fromFolder); rejoinCfg = new BotRejoinConfig(); } protected void RestartTimer(object o, ElapsedEventArgs e) { AutoRestartLoginTimer?.Stop(); AutoRestartLoginTimer = null; // Ensure logger is initialized early to avoid "logger not registered" errors var _ = OpenMetaverse.Logger.LogInstance; // Apply jitter before restart int jitter = new Random().Next(rejoinCfg.JitterMinMs, rejoinCfg.JitterMaxMs); LogFormater.Info($"Recovery sequence: Waiting {jitter}ms before restart (jitter applied)..."); Thread.Sleep(jitter); Restart(); } public bool IsLoaded() { return basicCfg.IsLoaded(); } public override void Start(bool updateEnabled = false, bool setEnabledTo = false) { running = true; LogFormater.Info("Client service [Starting]"); AutoRestartLoginTimer = new Timer { Interval = 30 * 1000, AutoReset = false }; AutoRestartLoginTimer.Elapsed += RestartTimer; Login(); } public override void Stop() { if(running == true) { LogFormater.Info("Client service [Stopping]"); } running = false; if (AutoRestartLoginTimer != null) { AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer = null; } if (client != null && client.Network != null && client.Network.Connected) { LogFormater.Warn("Service stopping - initiating logout"); flagLogoutExpected(); client.Network.Logout(); Thread.Sleep(3000); // Wait for packets to send } ResetClient(); } protected void HandleUnexpectedDisconnect(string reason) { if (getLogoutExpected()) return; int baseDelay = basicCfg.GetReconnectDelaySecs(); int maxDelay = basicCfg.GetMaxReconnectDelaySecs(); if (lastConnectTime != DateTime.MinValue && (DateTime.UtcNow - lastConnectTime).TotalSeconds > 60) { consecutiveLoginFailures = 0; } lastConnectTime = DateTime.MinValue; int delaySecs = (int)Math.Min(baseDelay * Math.Pow(2, consecutiveLoginFailures), maxDelay); if (isRegionRestartScheduled) { // Force a minimum 15 minutes (900 seconds) delay if a region restart is scheduled delaySecs = Math.Max(delaySecs, 900); } consecutiveLoginFailures++; LogFormater.Warn($"{reason} - retrying in {delaySecs}s (attempt {consecutiveLoginFailures})..."); StartRecoveryTimer(delaySecs * 1000); } protected void BotAlertMessage(object o, AlertMessageEventArgs e) { LogFormater.Info("Client service ~ Alert message: " + e.Message); if (e.Message != null && e.Message.IndexOf("restart", StringComparison.OrdinalIgnoreCase) >= 0) { LogFormater.Warn("Detected upcoming region restart. Initiating graceful logout to prevent limbo state."); isRegionRestartScheduled = true; flagLogoutExpected(); if (client != null && client.Network != null) { client.Network.Logout(); } // Directly start recovery timer with 15 minutes delay StartRecoveryTimer(900 * 1000); } } protected void BotLoggedOut(object o, LoggedOutEventArgs e) { LogFormater.Info("Client service ~ Logged out"); if (!getLogoutExpected()) { HandleUnexpectedDisconnect("Unexpected logout"); } master.TriggerBotClientEvent(true, !getLogoutExpected()); } protected void BotSimConnected(object o, SimConnectedEventArgs e) { if (AutoRestartLoginTimer != null) { AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer = null; } isRegionRestartScheduled = false; lastConnectTime = DateTime.UtcNow; goodticks = 0; LogFormater.Info("Client service ~ Connected to sim: " + e.Simulator.Name); } protected void BotSimDisconnected(object o, SimDisconnectedEventArgs e) { LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name); if (client?.Network?.CurrentSim != null && e.Simulator != client.Network.CurrentSim && client.Network.CurrentSim.Connected) { return; } goodticks = 0; if (!getLogoutExpected()) { HandleUnexpectedDisconnect("Unexpected sim disconnect"); } } protected void BotDisconnected(object o, DisconnectedEventArgs e) { LogFormater.Info("Client service ~ Network disconnected: " + e.Message); if (!getLogoutExpected()) { HandleUnexpectedDisconnect("Unexpected network disconnect: " + e.Message); } } protected void StartRecoveryTimer(int intervalMs = 5000) { if (AutoRestartLoginTimer == null) { AutoRestartLoginTimer = new Timer { Interval = intervalMs, AutoReset = false }; AutoRestartLoginTimer.Elapsed += RestartTimer; } else { AutoRestartLoginTimer.Interval = intervalMs; } AutoRestartLoginTimer.Stop(); AutoRestartLoginTimer.Start(); } protected void BotLoginStatus(object o, LoginProgressEventArgs e) { if (e.Status == LoginStatus.Failed || !string.IsNullOrEmpty(e.FailReason)) { string reason = string.IsNullOrEmpty(e.FailReason) ? e.Message : e.FailReason; LogFormater.Info("Client service ~ {FAILED} Login status: " + reason); client.Network.Logout(); ResetClient(); HandleUnexpectedDisconnect("Login failed: " + reason); return; } LogFormater.Info("Client service ~ Login status: " + e.Status.ToString()); } public void SendIM(UUID avatar, string message) { master.DataStoreService.BotRecordReplyIM(avatar,message); client.Self.InstantMessage(avatar, message); } public bool SendNotecard(string name, string content, UUID sendToUUID, bool attachDateTime=true, string subject=null) { bool returnstatus = true; if (attachDateTime == true) { name = name + " " + DateTime.Now; } if (subject == null) { subject = name + " Created via SecondBot notecard API"; } client.Inventory.RequestCreateItem( client.Inventory.FindFolderForType(AssetType.Notecard), name, subject, AssetType.Notecard, UUID.Random(), InventoryType.Notecard, PermissionMask.All, (bool Success, InventoryItem item) => { if (Success) { AssetNotecard empty = new() { BodyText = "\n" }; empty.Encode(); client.Inventory.RequestUploadNotecardAsset(empty.AssetData, item.UUID, (bool emptySuccess, string emptyStatus, UUID emptyItemID, UUID emptyAssetID) => { if (emptySuccess) { empty.BodyText = content; empty.Encode(); client.Inventory.RequestUploadNotecardAsset(empty.AssetData, emptyItemID, (bool finalSuccess, string finalStatus, UUID finalItemID, UUID finalID) => { if (finalSuccess) { LogFormater.Info("Sending notecard now"); client.Inventory.GiveItem(finalItemID, name, AssetType.Notecard, sendToUUID, false); } else { returnstatus = false; LogFormater.Warn("Unable to request notecard upload"); } }); } else { LogFormater.Warn("The fuck empty success notecard create"); returnstatus = false; } }); } else { LogFormater.Warn("Unable to find default notecards folder"); returnstatus = false; } } ); return returnstatus; } protected void ResetClient() { client = null; client = new GridClient(); client.Settings.STORE_LAND_PATCHES = true; client.Settings.MULTIPLE_SIMS = true; client.Network.SimConnected += BotSimConnected; client.Network.LoggedOut += BotLoggedOut; client.Network.Disconnected += BotDisconnected; client.Network.SimDisconnected += BotSimDisconnected; client.Network.LoginProgress += BotLoginStatus; client.Self.AlertMessage += BotAlertMessage; client.Self.AgentDataReply += (s, e) => { master.DataStoreService.SetActiveGroupTitle(e.GroupTitle); }; } public void Login() { ResetClient(); goodticks = 0; LogoutExpected = false; master.TriggerBotClientEvent(false, false); LoginParams loginParams = new( client, basicCfg.GetFirstName(), basicCfg.GetLastName(), basicCfg.GetPassword(), "secondbot", master.GetVersion() ); if (basicCfg.GetLoginURI() != "secondlife") { loginParams.URI = basicCfg.GetLoginURI(); } string customMac = basicCfg.GetMAC(); if (!string.IsNullOrEmpty(customMac)) { loginParams.MAC = customMac; } string customId0 = basicCfg.GetId0(); if (!string.IsNullOrEmpty(customId0)) { loginParams.ID0 = customId0; } string customStart = basicCfg.GetStartLocation(); if (!string.IsNullOrEmpty(customStart)) { loginParams.Start = customStart; } AutoRestartLoginTimer.Start(); client.Network.BeginLogin(loginParams); } protected int goodticks = 0; public override string Status() { if (client == null) { return "No Client [Restart needed]"; } else if (client.Network == null) { return "No network - [Starting / Shutting down]"; } else if (client.Network.Connected == false) { return "Not connected"; } else if (client.Network.CurrentSim == null) { return "No sim"; } if(goodticks == 6) { double G = basicCfg.GetDefaultHoverHeight(); GetClient().Self.SetHoverHeight(G); goodticks++; } else { goodticks++; if(goodticks == 120) { goodticks = 0; } } Vector3 pos = client.Self.SimPosition; string loc = "" + Math.Round(pos.X).ToString() + "," + Math.Round(pos.Y).ToString() + "," + Math.Round(pos.Z).ToString(); return client.Network.CurrentSim.Name + " " + loc; } } }