diff --git a/AGENTS.md b/AGENTS.md index c03bb0432..ada7b0356 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ The project is built using `MSBuild`. - Use `.\build.ps1` to take builds. - For execution policy issues use with `powershell.exe -ExecutionPolicy Bypass .\build.ps1` -- There are arguments that can be passed to the build script, `-Clean` for a clean build, `-Configuration` for the intended build configuration and `-DoNotStart` for not starting the main application. +- There are arguments that can be passed to the build script, `-Clean` for a clean build, `-Configuration` for the intended build configuration and `-Start` for starting the main application. - If you fail to make a build ask the user to build it. Check the `.\build.log` for any errors after user confirms the build. ## Instructions diff --git a/Application/RSBot.Controller/Program.cs b/Application/RSBot.Controller/Program.cs new file mode 100644 index 000000000..b516e2f69 --- /dev/null +++ b/Application/RSBot.Controller/Program.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommandLine; +using RSBot.IPC; + +namespace RSBot.Controller +{ + internal class Program + { + public class Options + { + [Option( + 'p', + "profile", + Required = false, + HelpText = "The profile name to target. Required unless --all is used." + )] + public string Profile { get; set; } + + [Option('c', "command", Required = true, HelpText = "The command to execute.")] + public string Command { get; set; } + + [Option('d', "data", Required = false, HelpText = "The data payload for the command.")] + public string Data { get; set; } + + [Option( + 'x', + "pipename", + Required = false, + HelpText = "The name of the pipe to connect to.", + Default = "RSBotIPC" + )] + public string PipeName { get; set; } + + [Option( + 'a', + "all", + Required = false, + HelpText = "Send command to all listening bot instances.", + Default = false + )] + public bool AllProfiles { get; set; } + } + + static async Task Main(string[] args) + { + await Parser + .Default.ParseArguments(args) + .WithParsedAsync(async opts => + { + if (!opts.AllProfiles && string.IsNullOrEmpty(opts.Profile)) + { + Console.WriteLine("Error: Either --profile or --all must be specified."); + return; + } + + if (!Enum.TryParse(opts.Command, true, out var commandType)) + { + Console.WriteLine($"Error: Invalid command '{opts.Command}'."); + return; + } + + var command = new IpcCommand + { + RequestId = Guid.NewGuid().ToString(), + CommandType = commandType, + Profile = opts.Profile, + Payload = opts.Data, + TargetAllProfiles = opts.AllProfiles, + }; + + await ExecuteCommand(command, opts.PipeName); + }); + } + + static async Task ExecuteCommand(IpcCommand command, string pipeName) + { + var pipeClient = new NamedPipeClient(pipeName, Console.WriteLine); + var collectedResponses = new ConcurrentBag(); + var singleResponseTcs = new TaskCompletionSource(); + const int BATCH_COMMAND_TIMEOUT_MS = 5000; + + pipeClient.MessageReceived += (message) => + { + try + { + var response = IpcResponse.FromJson(message); + if (response != null && response.RequestId == command.RequestId) + { + collectedResponses.Add(response); + if (!command.TargetAllProfiles) + { + singleResponseTcs.TrySetResult(true); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing response: {ex.Message}"); + if (!command.TargetAllProfiles) + { + singleResponseTcs.TrySetResult(false); + } + } + }; + + pipeClient.Disconnected += () => + { + if (!command.TargetAllProfiles && !singleResponseTcs.Task.IsCompleted) + { + Console.WriteLine("Disconnected from server before receiving a response."); + singleResponseTcs.TrySetResult(false); + } + }; + + await pipeClient.ConnectAsync(); + + await pipeClient.SendMessageAsync(command.ToJson()); + + Task completedTask; + if (command.TargetAllProfiles) + { + // For batch commands, wait for a fixed duration to collect multiple responses + completedTask = await Task.WhenAny(Task.Delay(BATCH_COMMAND_TIMEOUT_MS), singleResponseTcs.Task); + } + else + { + // For single commands, wait for a single response or timeout + completedTask = await Task.WhenAny(singleResponseTcs.Task, Task.Delay(BATCH_COMMAND_TIMEOUT_MS)); + } + + if (completedTask == singleResponseTcs.Task && !singleResponseTcs.Task.Result) + { + Console.WriteLine("Failed to receive a valid response or disconnected unexpectedly."); + } + else if (collectedResponses.IsEmpty) + { + Console.WriteLine("Timeout waiting for a response from the server, or no responses received."); + } + else + { + Console.WriteLine($"--- Responses for Request ID: {command.RequestId} ---"); + foreach (var response in collectedResponses) + { + Console.WriteLine($" Success: {response.Success}"); + Console.WriteLine($" Message: {response.Message}"); + if (!string.IsNullOrEmpty(response.Payload)) + { + Console.WriteLine($" Payload: {response.Payload}"); + } + Console.WriteLine("-------------------------------------"); + } + } + + pipeClient.Disconnect(); + } + } +} diff --git a/Application/RSBot.Controller/RSBot.Controller.csproj b/Application/RSBot.Controller/RSBot.Controller.csproj new file mode 100644 index 000000000..1be63f325 --- /dev/null +++ b/Application/RSBot.Controller/RSBot.Controller.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + Exe + ..\..\Build\ + net8.0 + false + enable + enable + + diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs new file mode 100644 index 000000000..8867d083d --- /dev/null +++ b/Application/RSBot.Server/Program.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using CommandLine; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RSBot.IPC; + +namespace RSBot.Server +{ + internal class Program + { + public class TimestampedTextWriter : TextWriter + { + private readonly TextWriter _innerWriter; + + public TimestampedTextWriter(TextWriter innerWriter) + { + _innerWriter = innerWriter; + } + + public override Encoding Encoding => _innerWriter.Encoding; + + public override void Write(char value) + { + _innerWriter.Write(value); + } + + public override void Write(string value) + { + _innerWriter.Write($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {value}"); + } + + public override void WriteLine(string value) + { + _innerWriter.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {value}"); + } + } + + private static NamedPipeServer _serverPipe; + private static readonly ConcurrentDictionary _botClientConnections = + new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _cliRequestMap = + new ConcurrentDictionary(); + + public class Options + { + [Option( + 'x', + "pipename", + Required = false, + HelpText = "The name of the pipe to listen on.", + Default = "RSBotIPC" + )] + public string PipeName { get; set; } + } + + private static TaskCompletionSource _serverStopped = new TaskCompletionSource(); + + static async Task Main(string[] args) + { + SetupLogging("Server.log"); + await Parser + .Default.ParseArguments(args) + .WithParsedAsync(async o => + { + await RunServer(o.PipeName); + }); + } + + private static void SetupLogging(string logFileName) + { + string buildDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string logDirectory = Path.Combine(buildDirectory, "User", "Logs", "Environment"); + Directory.CreateDirectory(logDirectory); + + string logFilePath = Path.Combine(logDirectory, logFileName); + FileStream fileStream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + StreamWriter streamWriter = new StreamWriter(fileStream) { AutoFlush = true }; + TextWriter timestampedWriter = new TimestampedTextWriter(streamWriter); + + Console.SetOut(timestampedWriter); + Console.SetError(timestampedWriter); + } + + static async Task RunServer(string pipeName) + { + Console.WriteLine("RSBot IPC Server Starting..."); + + _serverPipe = new NamedPipeServer(pipeName); + _serverPipe.ClientConnected += OnClientConnected; + _serverPipe.ClientDisconnected += OnClientDisconnected; + _serverPipe.MessageReceived += async (clientPipeId, message) => + await OnMessageReceived(clientPipeId, message); + + _serverPipe.Start(); + + Console.WriteLine($"IPC Server listening on pipe: {pipeName}"); + await _serverStopped.Task; + + _serverPipe.Stop(); + Console.WriteLine("RSBot IPC Server Stopped."); + } + + private static void OnClientConnected(string clientPipeId) + { + Console.WriteLine($"Client connected: {clientPipeId}"); + } + + private static void OnClientDisconnected(string clientPipeId) + { + Console.WriteLine($"Client disconnected: {clientPipeId}"); + var botEntry = _botClientConnections.FirstOrDefault(x => x.Value == clientPipeId); + if (botEntry.Key != null) + { + _botClientConnections.TryRemove(botEntry.Key, out _); + Console.WriteLine($"Bot client '{botEntry.Key}' removed."); + } + var requestsToRemove = _cliRequestMap + .Where(kvp => kvp.Value == clientPipeId) + .Select(kvp => kvp.Key) + .ToList(); + foreach (var requestId in requestsToRemove) + { + _cliRequestMap.TryRemove(requestId, out _); + Console.WriteLine($"Removed pending request '{requestId}' for disconnected client."); + } + } + + private static async Task OnMessageReceived(string clientPipeId, string message) + { + Console.WriteLine($"Message received from {clientPipeId}: {message}"); + JObject jsonObject; + try + { + jsonObject = JObject.Parse(message); + } + catch (JsonException ex) + { + Console.WriteLine($"JsonException when parsing message as JObject: {ex.Message}. Message: {message}"); + return; + } + + if (jsonObject.ContainsKey("CommandType")) + { + try + { + IpcCommand command = jsonObject.ToObject(); + if (command != null) + { + if (command.CommandType == CommandType.RegisterBot) + { + string profileName = command.Profile; + if (!string.IsNullOrEmpty(profileName)) + { + _botClientConnections[profileName] = clientPipeId; + Console.WriteLine( + $"Bot client for profile '{profileName}' registered with pipe ID {clientPipeId}." + ); + } + else + { + Console.WriteLine( + $"Received RegisterBot command with empty profile from {clientPipeId}. Ignoring." + ); + } + } + else + { + lock (_cliRequestMap) + { + if (!string.IsNullOrEmpty(command.RequestId)) + { + _cliRequestMap[command.RequestId] = clientPipeId; + Console.WriteLine( + $"CLI client request '{command.RequestId}' mapped to {clientPipeId}." + ); + } + } + + Console.WriteLine( + $"Received command '{command.CommandType}' for profile '{command.Profile}' from CLI client {clientPipeId}" + ); + + if (command.TargetAllProfiles) + { + Console.WriteLine( + $"Broadcasting command '{command.CommandType}' to all connected bot clients." + ); + if (_botClientConnections.Any()) + { + foreach (var botPipeId in _botClientConnections.Values) + { + try + { + await _serverPipe.SendMessageToClientAsync(botPipeId, message); + Console.WriteLine( + $"Command '{command.CommandType}' successfully broadcasted to bot client {botPipeId}." + ); + } + catch (Exception ex) + { + Console.WriteLine( + $"Error broadcasting command '{command.CommandType}' to bot client {botPipeId}: {ex.Message}" + ); + } + } + } + else + { + Console.WriteLine( + "No bot clients connected to broadcast command to. Sending error response to CLI client." + ); + IpcResponse errorResponse = new IpcResponse + { + RequestId = command.RequestId, + Success = false, + Message = "No bot clients connected.", + Payload = "", + }; + await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); + lock (_cliRequestMap) + { + _cliRequestMap.TryRemove(command.RequestId, out _); + } + } + } + else + { + if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) + { + Console.WriteLine( + $"Routing command '{command.CommandType}' to bot client {botClientPipeId} for profile '{command.Profile}'." + ); + try + { + await _serverPipe.SendMessageToClientAsync(botClientPipeId, message); + Console.WriteLine( + $"Command '{command.CommandType}' successfully routed to bot client {botClientPipeId}." + ); + } + catch (Exception ex) + { + Console.WriteLine( + $"Error routing command '{command.CommandType}' to bot client {botClientPipeId}: {ex.Message}" + ); + IpcResponse errorResponse = new IpcResponse + { + RequestId = command.RequestId, + Success = false, + Message = + $"Error routing command to bot client for profile '{command.Profile}': {ex.Message}", + Payload = "", + }; + await _serverPipe.SendMessageToClientAsync( + clientPipeId, + errorResponse.ToJson() + ); + lock (_cliRequestMap) + { + _cliRequestMap.TryRemove(command.RequestId, out _); + } + } + } + else + { + Console.WriteLine( + $"Bot client for profile '{command.Profile}' not found. Sending error response to CLI client {clientPipeId}." + ); + IpcResponse errorResponse = new IpcResponse + { + RequestId = command.RequestId, + Success = false, + Message = $"Bot client for profile '{command.Profile}' not found.", + Payload = "", + }; + await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); + lock (_cliRequestMap) + { + _cliRequestMap.TryRemove(command.RequestId, out _); + } + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing message as IpcCommand: {ex.Message}. Message: {message}"); + } + } + else if (jsonObject.ContainsKey("Success")) + { + try + { + IpcResponse response = jsonObject.ToObject(); + if (response != null && !string.IsNullOrEmpty(response.RequestId)) + { + Console.WriteLine( + $"Received response for request '{response.RequestId}' from bot client {clientPipeId}" + ); + + string cliClientPipeId = null; + bool foundClient = false; + + lock (_cliRequestMap) + { + if (_cliRequestMap.TryRemove(response.RequestId, out cliClientPipeId)) + { + foundClient = true; + } + } + + if (foundClient) + { + Console.WriteLine( + $"Routing response for request '{response.RequestId}' back to CLI client {cliClientPipeId}." + ); + try + { + Console.WriteLine( + $"Attempting to send response to CLI client {cliClientPipeId}: {message}" + ); + await _serverPipe.SendMessageToClientAsync(cliClientPipeId, message); + Console.WriteLine($"Response successfully sent to CLI client {cliClientPipeId}."); + } + catch (Exception ex) + { + Console.WriteLine( + $"Error sending response to CLI client {cliClientPipeId}: {ex.Message}" + ); + } + } + else + { + Console.WriteLine( + $"Could not find originating CLI client for request ID '{response.RequestId}'. Message: {message}" + ); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing message as IpcResponse: {ex.Message}. Message: {message}"); + } + } + else + { + Console.WriteLine($"Unknown message type received from {clientPipeId}: {message}"); + } + } + } +} diff --git a/Application/RSBot.Server/RSBot.Server.csproj b/Application/RSBot.Server/RSBot.Server.csproj new file mode 100644 index 000000000..b04b64815 --- /dev/null +++ b/Application/RSBot.Server/RSBot.Server.csproj @@ -0,0 +1,16 @@ + + + + + + + + + WinExe + ..\..\Build\ + net8.0 + false + enable + enable + + diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index 7e4815297..f56576e34 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text; @@ -7,12 +8,17 @@ using CommandLine.Text; using RSBot.Core; using RSBot.Core.Components; +using RSBot.General.Components; +using RSBot.General.Models; using RSBot.Views; namespace RSBot; internal static class Program { + public static Main MainForm { get; private set; } + private static string _commandLineSelectAutologinUsername; + public static string AssemblyTitle = Assembly .GetExecutingAssembly() .GetCustomAttribute() @@ -33,6 +39,35 @@ public class CommandLineOptions [Option('p', "profile", Required = false, HelpText = "Set the profile name to use.")] public string Profile { get; set; } + + [Option('l', "listen", Required = false, HelpText = "Enable IPC and listen on the specified pipe name.")] + public string Listen { get; set; } + + [Option('e', "create-autologin", Required = false, HelpText = "Create a new autologin entry.")] + public bool CreateAutologinEntry { get; set; } + + [Option("username", Required = false, HelpText = "Username for the autologin entry.")] + public string Username { get; set; } + + [Option("password", Required = false, HelpText = "Password for the autologin entry.")] + public string Password { get; set; } + + [Option("secondary-password", Required = false, HelpText = "Secondary password for the autologin entry.")] + public string SecondaryPassword { get; set; } + + [Option("provider-name", Required = false, HelpText = "Provider name (e.g., Joymax, JCPlanet).")] + public string ProviderName { get; set; } + + [Option("server", Required = false, HelpText = "Server name for the autologin entry.")] + public string Server { get; set; } + + [Option( + 'a', + "select-autologin", + Required = false, + HelpText = "Select an existing autologin entry by username." + )] + public string SelectAutologin { get; set; } } private static void DisplayHelp(ParserResult result) @@ -83,11 +118,18 @@ private static void Main(string[] args) Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); - Main mainForm = new Main(); - SplashScreen splashScreen = new SplashScreen(mainForm); + MainForm = new Main(); + SplashScreen splashScreen = new SplashScreen(MainForm); splashScreen.ShowDialog(); - Application.Run(mainForm); + if (!string.IsNullOrEmpty(_commandLineSelectAutologinUsername)) + { + GlobalConfig.Set("RSBot.General.AutoLoginAccountUsername", _commandLineSelectAutologinUsername); + GlobalConfig.Save(); + Log.Debug($"Autologin entry '{_commandLineSelectAutologinUsername}' applied and saved from command line."); + } + + Application.Run(MainForm); } private static void RunOptions(CommandLineOptions options) @@ -110,5 +152,72 @@ private static void RunOptions(CommandLineOptions options) ProfileManager.SelectedCharacter = character; Log.Debug($"Selected character by args: {character}"); } + + if (options.CreateAutologinEntry) + { + if (string.IsNullOrEmpty(options.Username) || string.IsNullOrEmpty(options.Password)) + { + Log.Error("Username and Password are required to create an autologin entry."); + Environment.Exit(1); + } + + // Ensure accounts are loaded before trying to add to them + Accounts.Load(); + + byte channel = 0; + if (!string.IsNullOrEmpty(options.ProviderName)) + { + switch (options.ProviderName.ToLowerInvariant()) + { + case "joymax": + channel = 1; + break; + case "jcplanet": + channel = 2; + break; + default: + Log.Error($"Unrecognized provider name '{options.ProviderName}'. Supported: Joymax, JCPlanet."); + Environment.Exit(1); + break; + } + } + // Default to Joymax if no provider name is specified, matching UI default behavior. + if (channel == 0) + channel = 1; + + var newAccount = new Account + { + Username = options.Username, + Password = options.Password, + SecondaryPassword = options.SecondaryPassword, + Servername = options.Server, + Channel = channel, + Characters = new List(), // Initialize empty character list + }; + + // Check if an account with the same username already exists + var existingAccount = Accounts.SavedAccounts.Find(a => a.Username == newAccount.Username); + if (existingAccount != null) + { + Log.Warn($"Autologin entry for username '{newAccount.Username}' already exists. Updating it."); + Accounts.SavedAccounts.Remove(existingAccount); // Remove old entry + } + + Accounts.SavedAccounts.Add(newAccount); + Accounts.Save(); + Log.Debug($"Autologin entry for '{newAccount.Username}' created/updated successfully."); + Environment.Exit(0); // Exit after creating autologin entry + } + + if (!string.IsNullOrEmpty(options.Listen)) + { + IpcManager.Initialize(options.Listen); + } + + if (!string.IsNullOrEmpty(options.SelectAutologin)) + { + _commandLineSelectAutologinUsername = options.SelectAutologin; + Log.Debug($"Autologin entry '{options.SelectAutologin}' marked for selection."); + } } } diff --git a/Application/RSBot/RSBot.csproj b/Application/RSBot/RSBot.csproj index fea695e8b..ae88de618 100644 --- a/Application/RSBot/RSBot.csproj +++ b/Application/RSBot/RSBot.csproj @@ -27,6 +27,7 @@ True + diff --git a/Application/RSBot/Views/Main.cs b/Application/RSBot/Views/Main.cs index 620e46d13..d2214e9f0 100644 --- a/Application/RSBot/Views/Main.cs +++ b/Application/RSBot/Views/Main.cs @@ -130,6 +130,27 @@ private void RegisterEvents() EventManager.SubscribeEvent("OnAgentServerDisconnected", OnAgentServerDisconnected); EventManager.SubscribeEvent("OnShowScriptRecorder", new Action(OnShowScriptRecorder)); EventManager.SubscribeEvent("OnAddSidebarElement", new Action(OnAddSidebarElement)); + EventManager.SubscribeEvent("OnSetVisibility", new Action(OnSetVisibility)); + EventManager.SubscribeEvent("OnGoClientless", OnGoClientless); + } + + private void OnGoClientless() + { + ClientlessManager.GoClientless(); + } + + private void OnSetVisibility(bool visible) + { + if (InvokeRequired) + { + Invoke(new Action(OnSetVisibility), visible); + return; + } + + if (visible) + Show(); + else + Hide(); } private void OnAddSidebarElement(Control obj) diff --git a/Library/RSBot.Core/Bot.cs b/Library/RSBot.Core/Bot.cs index b8c3ba0d1..df36eaba6 100644 --- a/Library/RSBot.Core/Bot.cs +++ b/Library/RSBot.Core/Bot.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using RSBot.Core.Components; using RSBot.Core.Event; @@ -29,6 +30,16 @@ public class Bot /// public IBotbase Botbase { get; private set; } + /// + /// Gets the start time. + /// + public DateTime StartTime { get; private set; } + + /// + /// Gets the uptime. + /// + public TimeSpan Uptime => Running ? DateTime.Now - StartTime : TimeSpan.Zero; + /// /// Sets the botbase. /// @@ -49,6 +60,7 @@ public void Start() if (Running || Botbase == null) return; + StartTime = DateTime.Now; TokenSource = new CancellationTokenSource(); Task.Factory.StartNew( diff --git a/Library/RSBot.Core/Components/IpcManager.cs b/Library/RSBot.Core/Components/IpcManager.cs new file mode 100644 index 000000000..ac0c415b6 --- /dev/null +++ b/Library/RSBot.Core/Components/IpcManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Threading.Tasks; +using RSBot.Core.Event; +using RSBot.IPC; + +namespace RSBot.Core.Components +{ + public class IpcManager + { + private static NamedPipeClient _pipeClient; + private static string _pipeName; + + public static void Initialize(string pipeName) + { + if (string.IsNullOrEmpty(pipeName)) + return; + + _pipeName = pipeName; + _pipeClient = new NamedPipeClient(_pipeName, Log.Debug); + _pipeClient.Connected += OnConnected; + _pipeClient.Disconnected += OnDisconnected; + _pipeClient.MessageReceived += OnMessageReceived; + + _ = _pipeClient.ConnectAsync(); + } + + private static async void OnConnected() + { + Log.Debug("IPC: Connected to server."); + + var profileName = ProfileManager.SelectedProfile; + if (!string.IsNullOrEmpty(profileName)) + { + var command = new IpcCommand + { + CommandType = CommandType.RegisterBot, + Profile = profileName, + RequestId = Guid.NewGuid().ToString(), + }; + await _pipeClient.SendMessageAsync(command.ToJson()); + Log.Debug($"IPC: Sent registration for profile '{profileName}'."); + } + } + + private static void OnDisconnected() + { + Log.Debug("IPC: Disconnected from server. Reconnecting..."); + Task.Delay(5000).ContinueWith(t => _pipeClient.ConnectAsync()); + } + + private static async void OnMessageReceived(string message) + { + Log.Debug($"IPC: Message received: {message}"); + + try + { + IpcCommand command = IpcCommand.FromJson(message); + if (command != null) + { + IpcResponse response = await HandleCommand(command); + + if (response != null) + { + await _pipeClient.SendMessageAsync(response.ToJson()); + } + } + } + catch (Exception ex) + { + Log.Error($"IPC: Error processing message: {ex.Message}"); + } + } + + private static async Task HandleCommand(IpcCommand command) + { + var response = new IpcResponse + { + RequestId = command.RequestId, + Success = true, + Message = "Command processed.", + }; + + switch (command.CommandType) + { + case CommandType.Stop: + Kernel.Bot.Stop(); + response.Message = "Bot stopped."; + break; + + case CommandType.Start: + Kernel.Bot.Start(); + response.Message = "Bot started."; + break; + + case CommandType.GetInfo: + response.Payload = new + { + Profile = ProfileManager.SelectedProfile, + Character = Game.Player?.Name, + Location = Game.Player?.Position.ToString(), + Uptime = Kernel.Bot.Uptime, + Botbase = Kernel.Bot.Botbase?.Name, + ClientVisible = !Game.Clientless, + }.ToString(); + break; + + case CommandType.SetVisibility: + bool visible = bool.Parse(command.Payload); + EventManager.FireEvent("OnSetVisibility", visible); + response.Message = $"Window visibility set to {visible}."; + break; + + case CommandType.GoClientless: + EventManager.FireEvent("OnGoClientless"); + response.Message = "Switched to clientless mode."; + break; + + case CommandType.SetClientVisibility: + bool clientVisible = bool.Parse(command.Payload); + ClientManager.SetVisible(clientVisible); + response.Message = $"Client window visibility set to {clientVisible}."; + break; + + case CommandType.LaunchClient: + var started = await ClientManager.Start(); + response.Success = started; + response.Message = started ? "Client launched successfully." : "Failed to launch client."; + break; + + case CommandType.KillClient: + ClientManager.Kill(); + response.Message = "Client killed."; + break; + + default: + response.Success = false; + response.Message = $"Unknown command type: {command.CommandType}"; + break; + } + + return response; + } + } +} diff --git a/Library/RSBot.Core/Config/GlobalConfig.cs b/Library/RSBot.Core/Config/GlobalConfig.cs index d50ed972e..1613ab160 100644 --- a/Library/RSBot.Core/Config/GlobalConfig.cs +++ b/Library/RSBot.Core/Config/GlobalConfig.cs @@ -23,10 +23,7 @@ public static void Load() _config = new Config(path); // Migration: PR #934 "RSBot.Default" was moved to "RSBot.Training" - if ( - _config.Exists("RSBot.BotName") - && _config.Get("RSBot.BotName") == "RSBot.Default" - ) + if (_config.Exists("RSBot.BotName") && _config.Get("RSBot.BotName") == "RSBot.Default") { _config.Set("RSBot.BotName", "RSBot.Training"); _config.Save(); diff --git a/Library/RSBot.Core/RSBot.Core.csproj b/Library/RSBot.Core/RSBot.Core.csproj index 0a46a2b24..6011191b9 100644 --- a/Library/RSBot.Core/RSBot.Core.csproj +++ b/Library/RSBot.Core/RSBot.Core.csproj @@ -20,5 +20,6 @@ + diff --git a/Library/RSBot.IPC/CommandType.cs b/Library/RSBot.IPC/CommandType.cs new file mode 100644 index 000000000..156adfc72 --- /dev/null +++ b/Library/RSBot.IPC/CommandType.cs @@ -0,0 +1,23 @@ +namespace RSBot.IPC +{ + public enum CommandType + { + RegisterBot, + Stop, + Start, + GetInfo, + SetVisibility, + GoClientless, + SetClientVisibility, + LaunchClient, + KillClient, + SetOptions, + CreateAutologin, + SelectAutologin, + CopyProfile, + Move, + SpecifyTrainingArea, + SelectBotbase, + ReturnToTown, + } +} diff --git a/Library/RSBot.IPC/IpcCommand.cs b/Library/RSBot.IPC/IpcCommand.cs new file mode 100644 index 000000000..a8aa3d91c --- /dev/null +++ b/Library/RSBot.IPC/IpcCommand.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using RSBot.IPC; + +namespace RSBot.IPC +{ + public class IpcCommand + { + public string RequestId { get; set; } + public CommandType CommandType { get; set; } + public string Profile { get; set; } + public string Payload { get; set; } + public bool TargetAllProfiles { get; set; } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + + public static IpcCommand FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + } +} diff --git a/Library/RSBot.IPC/IpcResponse.cs b/Library/RSBot.IPC/IpcResponse.cs new file mode 100644 index 000000000..72a15672e --- /dev/null +++ b/Library/RSBot.IPC/IpcResponse.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace RSBot.IPC +{ + public class IpcResponse + { + public string RequestId { get; set; } + public bool Success { get; set; } + public string Message { get; set; } + public string Payload { get; set; } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + + public static IpcResponse FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + } +} diff --git a/Library/RSBot.IPC/NamedPipeClient.cs b/Library/RSBot.IPC/NamedPipeClient.cs new file mode 100644 index 000000000..6c95ca029 --- /dev/null +++ b/Library/RSBot.IPC/NamedPipeClient.cs @@ -0,0 +1,106 @@ +using System; +using System.IO.Pipes; +using System.Text; +using System.Threading.Tasks; + +namespace RSBot.IPC +{ + public class NamedPipeClient + { + private readonly string _pipeName; + private NamedPipeClientStream _pipeClient; + private readonly Action _logger; + + public event Action MessageReceived; + public event Action Connected; + public event Action Disconnected; + + public NamedPipeClient(string pipeName, Action logger = null) + { + _pipeName = pipeName; + _logger = logger; + } + + private void Log(string message) + { + _logger?.Invoke(message); + } + + public async Task ConnectAsync() + { + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + try + { + Log($"Attempting to connect to pipe {_pipeName}..."); + await _pipeClient.ConnectAsync(5000); + Log("Successfully connected to pipe."); + Connected?.Invoke(); + _ = ReadMessagesAsync(); + } + catch (TimeoutException) + { + Log($"Could not connect to pipe {_pipeName}. Timeout."); + Disconnected?.Invoke(); + } + catch (Exception ex) + { + Log($"Error connecting to pipe {_pipeName}: {ex.Message}"); + Disconnected?.Invoke(); + } + } + + public async Task SendMessageAsync(string message) + { + if (_pipeClient != null && _pipeClient.IsConnected) + { + byte[] buffer = Encoding.UTF8.GetBytes(message); + await _pipeClient.WriteAsync(buffer, 0, buffer.Length); + _pipeClient.WaitForPipeDrain(); + } + else + { + Log("Client not connected. Cannot send message."); + } + } + + private async Task ReadMessagesAsync() + { + byte[] buffer = new byte[4096]; + try + { + while (_pipeClient.IsConnected) + { + int bytesRead = await _pipeClient.ReadAsync(buffer, 0, buffer.Length); + if (bytesRead > 0) + { + string message = Encoding.UTF8.GetString(buffer, 0, bytesRead); + MessageReceived?.Invoke(message); + } + else if (bytesRead == 0) + { + break; + } + } + } + catch (Exception ex) + { + Log($"Error reading from pipe: {ex.Message}"); + } + finally + { + Disconnect(); + } + } + + public void Disconnect() + { + if (_pipeClient != null) + { + _pipeClient.Close(); + _pipeClient.Dispose(); + _pipeClient = null; + Disconnected?.Invoke(); + } + } + } +} diff --git a/Library/RSBot.IPC/NamedPipeServer.cs b/Library/RSBot.IPC/NamedPipeServer.cs new file mode 100644 index 000000000..06d7575f9 --- /dev/null +++ b/Library/RSBot.IPC/NamedPipeServer.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Concurrent; +using System.IO.Pipes; +using System.Text; +using System.Threading.Tasks; + +namespace RSBot.IPC +{ + public class NamedPipeServer + { + private readonly string _pipeName; + private readonly ConcurrentDictionary _clientPipesMap = + new ConcurrentDictionary(); + private bool _isRunning; + + public event Func MessageReceived; + public event Action ClientConnected; + public event Action ClientDisconnected; + + public NamedPipeServer(string pipeName) + { + _pipeName = pipeName; + } + + public void Start() + { + _isRunning = true; + Task.Run(ListenForConnections); + } + + public void Stop() + { + _isRunning = false; + foreach (var pipe in _clientPipesMap.Values) + { + pipe.Close(); + pipe.Dispose(); + } + _clientPipesMap.Clear(); + } + + private async Task ListenForConnections() + { + while (_isRunning) + { + try + { + Console.WriteLine("Creating pipe server stream..."); + NamedPipeServerStream pipeServer = new NamedPipeServerStream( + _pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous + ); + Console.WriteLine("Waiting for a client to connect..."); + await pipeServer.WaitForConnectionAsync(); + + if (_isRunning) + { + Console.WriteLine("Client connected."); + string clientPipeId = Guid.NewGuid().ToString(); + _clientPipesMap.TryAdd(clientPipeId, pipeServer); + ClientConnected?.Invoke(clientPipeId); + _ = HandleClientConnection(pipeServer, clientPipeId); + } + else + { + pipeServer.Close(); + pipeServer.Dispose(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error in ListenForConnections: {ex.Message}"); + } + } + } + + private async Task HandleClientConnection(NamedPipeServerStream pipeServer, string clientPipeId) + { + byte[] buffer = new byte[4096]; + try + { + while (pipeServer.IsConnected && _isRunning) + { + int bytesRead = await pipeServer.ReadAsync(buffer, 0, buffer.Length); + if (bytesRead > 0) + { + string message = Encoding.UTF8.GetString(buffer, 0, bytesRead); + MessageReceived?.Invoke(clientPipeId, message); + } + else if (bytesRead == 0) + { + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error handling client connection {clientPipeId}: {ex.Message}"); + } + finally + { + DisconnectClient(pipeServer, clientPipeId); + } + } + + public async Task SendMessageToClientAsync(string clientPipeId, string message) + { + if (_clientPipesMap.TryGetValue(clientPipeId, out NamedPipeServerStream pipe)) + { + if (pipe.IsConnected) + { + try + { + byte[] buffer = Encoding.UTF8.GetBytes(message); + await pipe.WriteAsync(buffer, 0, buffer.Length); + pipe.WaitForPipeDrain(); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message to client {clientPipeId}: {ex.Message}"); + } + } + else + { + Console.WriteLine($"Client {clientPipeId} is not connected. Cannot send message."); + } + } + else + { + Console.WriteLine($"Client {clientPipeId} not found in map. Cannot send message."); + } + } + + private void DisconnectClient(NamedPipeServerStream pipeServer, string clientPipeId) + { + if (_clientPipesMap.TryRemove(clientPipeId, out _)) + { + pipeServer.Close(); + pipeServer.Dispose(); + ClientDisconnected?.Invoke(clientPipeId); + } + } + } +} diff --git a/Library/RSBot.IPC/RSBot.IPC.csproj b/Library/RSBot.IPC/RSBot.IPC.csproj new file mode 100644 index 000000000..76329e4ac --- /dev/null +++ b/Library/RSBot.IPC/RSBot.IPC.csproj @@ -0,0 +1,11 @@ + + + net8.0 + enable + enable + + + + + + diff --git a/Plugins/RSBot.General/Components/Accounts.cs b/Plugins/RSBot.General/Components/Accounts.cs index eb9bce32c..88fae2755 100644 --- a/Plugins/RSBot.General/Components/Accounts.cs +++ b/Plugins/RSBot.General/Components/Accounts.cs @@ -10,7 +10,7 @@ namespace RSBot.General.Components; -internal class Accounts +public class Accounts { /// /// Gets or sets the saved accounts. diff --git a/Plugins/RSBot.General/Models/Account.cs b/Plugins/RSBot.General/Models/Account.cs index de4b3e913..a2531d43c 100644 --- a/Plugins/RSBot.General/Models/Account.cs +++ b/Plugins/RSBot.General/Models/Account.cs @@ -2,7 +2,7 @@ namespace RSBot.General.Models; -internal class Account +public class Account { /// /// Gets or sets the username. diff --git a/RSBot.sln b/RSBot.sln index 1151dfd94..b3cacd618 100644 --- a/RSBot.sln +++ b/RSBot.sln @@ -79,6 +79,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSBot.FileSystem", "Library EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSBot.NavMeshApi", "Library\RSBot.NavMeshApi\RSBot.NavMeshApi.csproj", "{E4DC5C65-9956-4696-9B89-043B20029EAA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RSBot.Server", "Application\RSBot.Server\RSBot.Server.csproj", "{5F6606A8-FE13-4DC4-8A71-A5373364F7AE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{50A17DA1-3556-4046-CEDC-33EB466D9C32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RSBot.IPC", "Library\RSBot.IPC\RSBot.IPC.csproj", "{006B34F7-AB6E-4767-9342-FE4870AE3683}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RSBot.Controller", "Application\RSBot.Controller\RSBot.Controller.csproj", "{021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -365,6 +373,42 @@ Global {E4DC5C65-9956-4696-9B89-043B20029EAA}.Release|x64.Build.0 = Release|Any CPU {E4DC5C65-9956-4696-9B89-043B20029EAA}.Release|x86.ActiveCfg = Release|Any CPU {E4DC5C65-9956-4696-9B89-043B20029EAA}.Release|x86.Build.0 = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|x64.Build.0 = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Debug|x86.Build.0 = Debug|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|Any CPU.Build.0 = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|x64.ActiveCfg = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|x64.Build.0 = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|x86.ActiveCfg = Release|Any CPU + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE}.Release|x86.Build.0 = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|Any CPU.Build.0 = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|x64.ActiveCfg = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|x64.Build.0 = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|x86.ActiveCfg = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Debug|x86.Build.0 = Debug|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|Any CPU.ActiveCfg = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|Any CPU.Build.0 = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|x64.ActiveCfg = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|x64.Build.0 = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|x86.ActiveCfg = Release|Any CPU + {006B34F7-AB6E-4767-9342-FE4870AE3683}.Release|x86.Build.0 = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|x64.ActiveCfg = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|x64.Build.0 = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|x86.ActiveCfg = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Debug|x86.Build.0 = Debug|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|Any CPU.Build.0 = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|x64.ActiveCfg = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|x64.Build.0 = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|x86.ActiveCfg = Release|Any CPU + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -393,6 +437,9 @@ Global {DDF07355-77E6-4A07-A83C-B90EE1B2A763} = {50ECA1E3-A0CF-4DC6-B0D9-AB5668D17EC3} {43159571-287D-42E1-B262-A39DE07D6763} = {06762F99-9EA8-4E3E-91E8-73F2ED402CFB} {E4DC5C65-9956-4696-9B89-043B20029EAA} = {06762F99-9EA8-4E3E-91E8-73F2ED402CFB} + {5F6606A8-FE13-4DC4-8A71-A5373364F7AE} = {AC1664CE-FD32-4A07-8C5C-0C96D973960C} + {006B34F7-AB6E-4767-9342-FE4870AE3683} = {50A17DA1-3556-4046-CEDC-33EB466D9C32} + {021E9CA2-D4F2-4F3C-BEA6-14BBDA0FF49C} = {AC1664CE-FD32-4A07-8C5C-0C96D973960C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9E2A0162-C6BF-4792-97DC-C8824F69B98C} diff --git a/build.ps1 b/build.ps1 index 2be4fe6ad..247ef5430 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,22 +1,24 @@ -# Usage: -# `build.ps1 -# -Clean[False, optional] -# -DoNotStart[False, optional] -# -Configuration[Debug, optional]` - param( [string]$Configuration = "Debug", [switch]$Clean, - [switch]$DoNotStart + [switch]$CleanRepo, + [switch]$Start ) +if ($CleanRepo) { + Write-Output "Performing a full clean build..." + git clean -dfX +} + if (-not (Test-Path ".\SDUI")) { Write-Output "SDUI submodule is missing. Initializing and updating submodules..." git submodule update --init --recursive } -taskkill /F /IM RSBot.exe taskkill /F /IM sro_client.exe +taskkill /F /IM RSBot.exe +taskkill /F /IM RSBot.Server.exe +taskkill /F /IM RSBot.Controller.exe if ($Clean) { Write-Output "Performing a clean build..." @@ -37,7 +39,7 @@ if ($Clean) { Remove-Item -Recurse -Force ".\temp" -ErrorAction SilentlyContinue > $null } -if (!$DoNotStart) { +if ($Start) { Write-Output "Starting RSBot..." & ".\Build\RSBot.exe" -} \ No newline at end of file +} diff --git a/vscode.code-workspace b/vscode.code-workspace new file mode 100644 index 000000000..e3ef86f8c --- /dev/null +++ b/vscode.code-workspace @@ -0,0 +1,31 @@ +{ + "folders": [ + { "path": ".", "name": "root" }, + { "path": "Build" }, + { "path": "Build/User" }, + { "path": "Application" }, + { "path": "Botbases" }, + { "path": "Library" }, + { "path": "Plugins" } + ], + "settings": { + "files.exclude": { + ".github": true, + ".editorconfig": true, + ".gitattributes": true, + ".gitignore": true, + ".gitmodules": true, + "LICENSE": true, + "README.md": true, + "AGREEMENT.md": true, + "**/obj": true, + "**/bin": true, + "**/*.dll": true, + "**/*.pdb": true, + "**/*.lib": true, + "**/*.deps.json": true, + "**/*.runtimeconfig.json": true, + "runtimes": true + } + } +}