Shortnote

The client is a bot that can be run on the command-line and is controlled by the controller script from in-world. The client requires a configuration that is stored in Simscape.config in the same directory as the executable.

A sample configuration file looks like this:

firstname:Simscape
lastname:Sextant
password:$1$a21bb8e950fda536faa10fc753a68a3d
grid:https://login.agni.lindenlab.com/cgi-bin/login.cgi
secret:PASSWORD
master:52ec55a8-5792-4ec6-abc5-b92b6ae37c59

Where the following parameters are:

For Second Life, everything aside from the grid must be changed to your set-up.

Code

Simscape.cs
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2012 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Net;
using System.Threading;
using System.Collections.Generic;
using OpenMetaverse;
using System.Linq;
 
namespace Simscape {
  internal class Simscape {
    private static readonly GridClient Client = new GridClient();
    private static string _firstName = string.Empty;
    private static string _lastName = string.Empty;
    private static string _password = string.Empty;
    private static List < UUID > _masters = new List < UUID > ();
    private static readonly Thread MainConnectionThread = new Thread(MainConnection);
    private static readonly Thread StatisticsThread = new Thread(SendStatistics);
    private static Dictionary < char, bool > StabilizeChecks = new Dictionary < char, bool > () {
      {
        'l', false
      }, {
        'e', false
      }, {
        's', false
      }, {
        'a', false
      }
    };
    private static List < string > AuthAgents = new List < string > ();
    private static volatile bool _run = true;
    private static volatile bool _runStatistics = true;
    private static volatile string _url = string.Empty;
 
    public static void Main() {
 
      Settings.LOG_LEVEL = Helpers.LogLevel.None;
      Client.Settings.STORE_LAND_PATCHES = true;
      Client.Settings.ALWAYS_DECODE_OBJECTS = true;
      Client.Settings.ALWAYS_REQUEST_OBJECTS = true;
      Client.Settings.SEND_AGENT_UPDATES = true;
      Client.Settings.USE_ASSET_CACHE = false;
      Client.Settings.FETCH_MISSING_INVENTORY = true;
 
      Client.Network.LoginProgress += HandleLoginProgress;
      Client.Appearance.AppearanceSet += HandleAppearanceSet;
      Client.Network.SimConnected += HandleSimulatorConnected;
      Client.Network.Disconnected += HandleDisconnected;
      Client.Network.SimDisconnected += HandleSimulatorDisconnected;
      Client.Network.EventQueueRunning += HandleEventQueueRunning;
 
      Client.Friends.FriendshipOffered += HandleFriendshipOffered;
      Client.Self.IM += HandleSelfIM;
 
      try {
        foreach(string cfg in File.ReadLines("Simscape.config")) {
          string[] data = cfg.Split(new char[] {
            ':'
          }, 2);
          switch (data[0]) {
          case "firstname":
            _firstName = data[1];
            break;
          case "lastname":
            _lastName = data[1];
            break;
          case "password":
            _password = data[1];
            break;
          case "grid":
            Client.Settings.LOGIN_SERVER = data[1];
            break;
            // auth-secrets and masters are read dynamically on request.
          case "master":
            break;
          case "secret":
            break;
          default:
            Console.WriteLine("[Simscape] : Garbled line: " + cfg + ", the program will now terminate.");
            System.Environment.Exit(1);
            break;
          }
        }
      } catch (FileNotFoundException) {
        Console.WriteLine("[Simscape] : Could not read configuration file.");
        System.Environment.Exit(1);
      }
 
      new List < int > (new int[Console.WindowWidth]).ForEach(i => Console.Write("-"));
      MainConnectionThread.Start();
    }
 
    private static void HandleDisconnected(object sender, DisconnectedEventArgs e) {
      Console.Write("\n[Simscape] : Disconnected...");
      if (!StabilizeChecks.ContainsKey('l')) {
        StabilizeChecks.Add('l', false);
        return;
      }
      StabilizeChecks['l'] = false;
    }
 
    private static void HandleEventQueueRunning(object sender, EventQueueRunningEventArgs e) {
      Console.Write("\n[Simscape] : Event queue started...");
      if (!StabilizeChecks.ContainsKey('e')) {
        StabilizeChecks.Add('e', true);
        return;
      }
      StabilizeChecks['e'] = true;
    }
 
    private static void HandleSimulatorConnected(object sender, SimConnectedEventArgs e) {
      Console.Write("\n[Simscape] : Simulator connected...");
      if (!StabilizeChecks.ContainsKey('s')) {
        StabilizeChecks.Add('s', true);
        return;
      }
      StabilizeChecks['s'] = true;
    }
 
    private static void HandleSimulatorDisconnected(object sender, SimDisconnectedEventArgs e) {
      if (Client.Network.Simulators.Count >= 1) return;
      Console.Write("\n[Simscape] : Simulators disconnected...");
      if (!StabilizeChecks.ContainsKey('s')) {
        StabilizeChecks.Add('s', false);
        return;
      }
      StabilizeChecks['s'] = false;
    }
 
    private static void HandleAppearanceSet(object sender, AppearanceSetEventArgs e) {
      if (e.Success) {
        Console.Write("\n[Simscape] : Appearance set...");
        if (!StabilizeChecks.ContainsKey('a')) {
          StabilizeChecks.Add('a', true);
          return;
        }
        StabilizeChecks['a'] = true;
        return;
      }
      Console.Write("\n[Simscape] : Failed to set appearance...");
      if (!StabilizeChecks.ContainsKey('a')) {
        StabilizeChecks.Add('a', false);
        return;
      }
      StabilizeChecks['a'] = false;
    }
 
    private static void HandleLoginProgress(object sender, LoginProgressEventArgs e) {
      switch (e.Status) {
      case LoginStatus.Success:
        Console.Write("\n[Simscape] : Login ok...");
        StabilizeChecks['l'] = true;
        break;
      case LoginStatus.Failed:
        Console.Write("\n[Simscape] : Failed Login...");
        if (!StabilizeChecks.ContainsKey('l')) {
          StabilizeChecks.Add('l', false);
          break;
        }
        StabilizeChecks['l'] = false;
        break;
      }
    }
 
    private static void HandleFriendshipOffered(object sender, FriendshipOfferedEventArgs e) {
      if (!_masters.Contains(e.AgentID)) return;
      Console.Write("\n[Simscape] : Accepting friendship with " + e.AgentName + ".");
      Client.Friends.AcceptFriendship(e.AgentID, e.SessionID);
    }
 
    private static void MainConnection() {
      StabilizeChecks = new Dictionary < char, bool > ();
      Client.Network.BeginLogin(new LoginParams(Client, _firstName, _lastName, _password, "[WaS] Corrade", "1.0"));
      Console.Write("\n[Corrade] : Stabilizing, please wait...");
      do {
        Console.Write(".");
        Thread.Sleep(1000);
      } while (StabilizeChecks.Count != 4 && !StabilizeChecks.ContainsValue(false));
      if (!StabilizeChecks.ContainsValue(false)) Console.Write("\n[Corrade] : Fully connected and accepting commands.");
      do {
        Thread.Sleep(1000);
        if (StabilizeChecks.ContainsValue(false)) {
          Console.Write("\n[Corrade] : Disconnected, shutting down...");
          _run = false;
        }
      }
      while (_run);
      Console.Write("\n[Corrade] : All operations completed. Quitting.");
      new List < int > (new int[Console.WindowWidth]).ForEach(i => Console.Write("-"));
      Client.Network.Logout();
      Client.Network.Shutdown(NetworkManager.DisconnectType.ClientInitiated);
    }
 
    private static void HandleSelfIM(object sender, InstantMessageEventArgs e) {
      if (e.IM.Dialog.Equals(InstantMessageDialog.MessageBox)) {
        Console.Write("\n[Simscape] : [Server Message] : " + e.IM.Message.Replace(System.Environment.NewLine, " "));
        return;
      }
      if (e.IM.Dialog.Equals(InstantMessageDialog.RequestTeleport)) {
        try {
          foreach(string cfg in File.ReadLines("Simscape.config")) {
            string[] data = cfg.Split(new char[] {
              ':'
            }, 3);
            switch (data[0]) {
            case "master":
              var master = UUID.Zero;
              if (!UUID.TryParse(data[1], out master)) continue;
              if (_masters.Contains(master)) continue;
              _masters.Add(master);
              break;
            }
          }
        } catch (FileNotFoundException) {
          Console.Write("\n[Simscape] : Could not read configuration file.");
          System.Environment.Exit(1);
        }
        if (!_masters.Contains(e.IM.FromAgentID)) return;
        Console.Write("\n[Simscape] : Accepting teleport lure.");
        Client.Self.TeleportLureRespond(e.IM.FromAgentID, e.IM.IMSessionID, true);
        return;
      }
      // Ignore group chat.
      if (e.IM.GroupIM) return;
      var msg = e.IM.Message.Split(new char[] {
        ' '
      });
      if (!msg[0].Equals("auth")) goto NoAuth;
      try {
        foreach(string cfg in File.ReadLines("Simscape.config")) {
          string[] data = cfg.Split(new char[] {
            ':'
          }, 3);
          switch (data[0]) {
          case "secret":
            if (!data[1].Equals(msg[1])) break;
            if (!AuthAgents.Contains(e.IM.FromAgentName)) AuthAgents.Add(e.IM.FromAgentName);
            Console.Write("\n[Simscape] : Authenticated: " + e.IM.FromAgentName + ".");
            break;
          }
        }
      } catch (FileNotFoundException) {
        Console.Write("\n[Simscape] : Could not read configuration file.");
        System.Environment.Exit(1);
      }
      return;
      NoAuth: foreach(var agent in AuthAgents) {
        if (!agent.Equals(e.IM.FromAgentID)) continue;
        goto Command;
      }
      return;
      Command: if (msg[0].Equals("o")) goto TeleportCommand;
      if (msg[0].Equals("r")) goto RestartCommand;
      return;
 
      TeleportCommand:
      // Terminate the statistics thread.
      if (StatisticsThread.ThreadState.Equals(ThreadState.Running)) {
        _runStatistics = false;
        StatisticsThread.Join();
      }
      // Get the URL.
      _url = msg[1];
      // Pop the command off the array.
      var cmdArgs = new List < string > (msg);
      cmdArgs.RemoveAt(0);
      // Pop the URL off the array.
      cmdArgs.RemoveAt(0);
      // Get the rest of the command.
      var _newSimulatorName = String.Join(" ", cmdArgs.ToArray());
      if (_newSimulatorName.Equals(Client.Network.CurrentSim.Name)) {
        Console.Write("\n[Simscape] : Already on simulator " + _newSimulatorName + ".");
        HTTPRequest(_url, "POST", "Teleport:FAILED");
        return;
      }
      var TeleportEvent = new ManualResetEvent(false);
      Client.Self.TeleportProgress += delegate(object tps, TeleportEventArgs et) {
        if (et.Status.Equals(TeleportStatus.Finished)) {
          TeleportEvent.Set();
        }
      };
      Client.Self.Fly(true);
      Console.Write("\n[Simscape] : Teleporting to " + _newSimulatorName + ".");
      Client.Self.Teleport(_newSimulatorName, new Vector3(128, 128, 30000));
      if (!TeleportEvent.WaitOne(10000, false)) {
        Console.Write("\n[Simscape] : Teleport failed to " + _newSimulatorName + ".");
        HTTPRequest(_url, "POST", "Teleport:FAILED");
        TeleportEvent.Reset();
        return;
      }
      TeleportEvent.Reset();
      Console.Write("\n[Simscape] : Teleport to " + _newSimulatorName + " successful.");
      HTTPRequest(_url, "POST", "Teleport:SUCCESS");
 
      _runStatistics = true;
      StatisticsThread.Start();
      return;
 
      RestartCommand: return;
 
    }
 
    private static void SendStatistics() {
      do {
        HTTPRequest(_url, "PUT", String.Format("[{0}]|Active scripts: {1}|Agents: {2}|Child agents: {3}|Dilation: {4}|FPS: {5}|Objects: {6}|Physics FPS: {7}|Physics time: {8}|Scripted Objects: {9}|CPURatio: {10}\n", Client.Network.CurrentSim.Name, Client.Network.CurrentSim.Stats.ActiveScripts, Client.Network.CurrentSim.Stats.Agents, Client.Network.CurrentSim.Stats.ChildAgents, Client.Network.CurrentSim.Stats.Dilation, Client.Network.CurrentSim.Stats.FPS, Client.Network.CurrentSim.Stats.Objects, Client.Network.CurrentSim.Stats.PhysicsFPS, Client.Network.CurrentSim.Stats.PhysicsTime, Client.Network.CurrentSim.Stats.ScriptedObjects, Client.Network.CurrentSim.CPURatio));
        Thread.Sleep(1000);
      } while (_runStatistics);
    }
 
    private static HttpWebResponse HTTPRequest(string url, string method, string data) {
      try {
        byte[] byteArray = Encoding.UTF8.GetBytes(data);
        WebRequest request = WebRequest.Create(url);
        request.Method = method;
        request.Timeout = 1000;
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = byteArray.Length;
        Stream dataStream = request.GetRequestStream();
        dataStream.Write(byteArray, 0, byteArray.Length);
        dataStream.Close();
        HttpWebResponse response = (HttpWebResponse) request.GetResponse();
        return response;
      } catch (WebException e) {
        return (HttpWebResponse) e.Response;
      }
    }
  }
}