SecondBot/SecondBotEvents/Services/BotClientService.cs
2026-06-13 04:30:13 -06:00

385 lines
14 KiB
C#

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)
{
goodticks = 0;
LogFormater.Info("Client service ~ Disconnected from sim: " + e.Simulator.Name);
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.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;
}
}
}