385 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|