From b6f15536ff7b98faa59862b6226b5fcfd89dc246 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Thu, 27 Nov 2025 14:51:56 +0300 Subject: [PATCH 1/9] Implement CleanRepo param to fully clean build artifacts --- build.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.ps1 b/build.ps1 index 2be4fe6a..1425ee46 100644 --- a/build.ps1 +++ b/build.ps1 @@ -7,9 +7,15 @@ param( [string]$Configuration = "Debug", [switch]$Clean, + [switch]$CleanRepo, [switch]$DoNotStart ) +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 @@ -40,4 +46,4 @@ if ($Clean) { if (!$DoNotStart) { Write-Output "Starting RSBot..." & ".\Build\RSBot.exe" -} \ No newline at end of file +} From 565a5b3fcf43932004b9cb418a23b30625beeac9 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Thu, 27 Nov 2025 17:10:22 +0300 Subject: [PATCH 2/9] WIP IPC Server, Controller --- Application/RSBot.Controller/Program.cs | 99 +++++++++++ .../RSBot.Controller/RSBot.Controller.csproj | 16 ++ Application/RSBot.Server/Program.cs | 161 ++++++++++++++++++ Application/RSBot.Server/RSBot.Server.csproj | 14 ++ Application/RSBot/Program.cs | 18 +- Application/RSBot/Views/Main.cs | 21 +++ Library/RSBot.Core/Bot.cs | 14 +- Library/RSBot.Core/Components/IpcManager.cs | 136 +++++++++++++++ Library/RSBot.Core/RSBot.Core.csproj | 1 + Library/RSBot.IPC/CommandType.cs | 22 +++ Library/RSBot.IPC/IpcCommand.cs | 23 +++ Library/RSBot.IPC/IpcResponse.cs | 22 +++ Library/RSBot.IPC/NamedPipeClient.cs | 107 ++++++++++++ Library/RSBot.IPC/NamedPipeServer.cs | 146 ++++++++++++++++ Library/RSBot.IPC/RSBot.IPC.csproj | 11 ++ RSBot.sln | 47 +++++ build.ps1 | 1 + 17 files changed, 854 insertions(+), 5 deletions(-) create mode 100644 Application/RSBot.Controller/Program.cs create mode 100644 Application/RSBot.Controller/RSBot.Controller.csproj create mode 100644 Application/RSBot.Server/Program.cs create mode 100644 Application/RSBot.Server/RSBot.Server.csproj create mode 100644 Library/RSBot.Core/Components/IpcManager.cs create mode 100644 Library/RSBot.IPC/CommandType.cs create mode 100644 Library/RSBot.IPC/IpcCommand.cs create mode 100644 Library/RSBot.IPC/IpcResponse.cs create mode 100644 Library/RSBot.IPC/NamedPipeClient.cs create mode 100644 Library/RSBot.IPC/NamedPipeServer.cs create mode 100644 Library/RSBot.IPC/RSBot.IPC.csproj diff --git a/Application/RSBot.Controller/Program.cs b/Application/RSBot.Controller/Program.cs new file mode 100644 index 00000000..54fbb41d --- /dev/null +++ b/Application/RSBot.Controller/Program.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using CommandLine; +using RSBot.IPC; + +namespace RSBot.Controller +{ + internal class Program + { + public class Options + { + [Option('p', "profile", Required = true, HelpText = "The profile name to target.")] + 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("pipename", Required = false, HelpText = "The name of the pipe to connect to.", Default = "RSBotIpcServer")] + public string PipeName { get; set; } + } + + static async Task Main(string[] args) + { + await Parser.Default.ParseArguments(args) + .WithParsedAsync(RunOptions); + } + + static async Task RunOptions(Options opts) + { + 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 + }; + + var pipeClient = new NamedPipeClient(opts.PipeName, Console.WriteLine); + bool responseReceived = false; + + pipeClient.MessageReceived += (message) => + { + try + { + var response = IpcResponse.FromJson(message); + if (response != null && response.RequestId == command.RequestId) + { + Console.WriteLine($"Success: {response.Success}"); + Console.WriteLine($"Message: {response.Message}"); + if (!string.IsNullOrEmpty(response.Payload)) + { + Console.WriteLine($"Payload: {response.Payload}"); + } + responseReceived = true; + pipeClient.Disconnect(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing response: {ex.Message}"); + responseReceived = true; + + } + }; + + pipeClient.Disconnected += () => + { + if (!responseReceived) + { + Console.WriteLine("Disconnected from server before receiving a response."); + } + }; + + await pipeClient.ConnectAsync(); + await pipeClient.SendMessageAsync(command.ToJson()); + + // Wait for a response or timeout + var timeout = Task.Delay(5000); // 5 second timeout + while (!responseReceived) + { + if (await Task.WhenAny(timeout) == timeout) + { + Console.WriteLine("Timeout waiting for a response from the server."); + break; + } + await Task.Delay(100); + } + } + } +} diff --git a/Application/RSBot.Controller/RSBot.Controller.csproj b/Application/RSBot.Controller/RSBot.Controller.csproj new file mode 100644 index 00000000..638efb85 --- /dev/null +++ b/Application/RSBot.Controller/RSBot.Controller.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + Exe + net8.0 + enable + enable + + diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs new file mode 100644 index 00000000..972dd80e --- /dev/null +++ b/Application/RSBot.Server/Program.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using CommandLine; +using Newtonsoft.Json; +using RSBot.IPC; + +namespace RSBot.Server +{ + internal class Program + { + private static NamedPipeServer _serverPipe; + private static readonly ConcurrentDictionary _botClientConnections = new ConcurrentDictionary(); // Profile -> ClientPipeId + private static readonly ConcurrentDictionary _cliRequestMap = new ConcurrentDictionary(); // RequestId -> CliClientPipeId + + public class Options + { + [Option("pipename", Required = false, HelpText = "The name of the pipe to listen on.", Default = "RSBotIpcServer")] + public string PipeName { get; set; } + } + + static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(o => + { + RunServer(o.PipeName); + }); + } + + static void RunServer(string pipeName) + { + Console.WriteLine("RSBot IPC Server Starting..."); + + _serverPipe = new NamedPipeServer(pipeName); + _serverPipe.ClientConnected += OnClientConnected; + _serverPipe.ClientDisconnected += OnClientDisconnected; + _serverPipe.MessageReceived += OnMessageReceived; + + _serverPipe.Start(); + + Console.WriteLine($"IPC Server listening on pipe: {pipeName}"); + Console.WriteLine("Press any key to stop the server."); + Console.ReadKey(); + + _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}"); + + // Remove from bot client connections + var botEntry = _botClientConnections.FirstOrDefault(x => x.Value == clientPipeId); + if (botEntry.Key != null) + { + _botClientConnections.TryRemove(botEntry.Key, out _); + Console.WriteLine($"Bot client '{botEntry.Key}' removed."); + } + + // Also check if the disconnected client has pending requests in the map + 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 void OnMessageReceived(string clientPipeId, string message) + { + Console.WriteLine($"Message received from {clientPipeId}: {message}"); + + // Try to parse as IpcCommand first + try + { + IpcCommand command = IpcCommand.FromJson(message); + if (command != null) + { + if (command.CommandType == CommandType.RegisterBot) + { + // This is a registration command from a bot client + string profileName = command.Profile; + if (!string.IsNullOrEmpty(profileName)) + { + _botClientConnections[profileName] = clientPipeId; + Console.WriteLine($"Bot client for profile '{profileName}' registered with pipe ID {clientPipeId}."); + } + } + else + { + // This is a command from a CLI client + if (!string.IsNullOrEmpty(command.RequestId)) + { + _cliRequestMap[command.RequestId] = clientPipeId; + } + + Console.WriteLine($"Received command '{command.CommandType}' for profile '{command.Profile}' from CLI client {clientPipeId}"); + + if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) + { + Console.WriteLine($"Routing command to bot client {botClientPipeId} for profile '{command.Profile}'"); + await _serverPipe.SendMessageToClientAsync(botClientPipeId, message); + } + else + { + // Bot client not found, send error response back to CLI client + 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()); + _cliRequestMap.TryRemove(command.RequestId, out _); // Clean up the request map + } + } + return; + } + } + catch (JsonException) { /* Not an IpcCommand, proceed to check if it's an IpcResponse */ } + + // Try to parse as IpcResponse + try + { + IpcResponse response = IpcResponse.FromJson(message); + if (response != null && !string.IsNullOrEmpty(response.RequestId)) + { + // This is a response from a bot client + Console.WriteLine($"Received response for request '{response.RequestId}' from bot client {clientPipeId}"); + + if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) + { + Console.WriteLine($"Routing response back to CLI client {cliClientPipeId}"); + await _serverPipe.SendMessageToClientAsync(cliClientPipeId, message); + } + else + { + Console.WriteLine($"Could not find originating CLI client for request ID '{response.RequestId}'."); + } + } + } + catch (JsonException) + { + Console.WriteLine($"Received unparseable message from {clientPipeId}: {message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error processing message from {clientPipeId}: {ex.Message}"); + } + } + } +} diff --git a/Application/RSBot.Server/RSBot.Server.csproj b/Application/RSBot.Server/RSBot.Server.csproj new file mode 100644 index 00000000..e95f17dd --- /dev/null +++ b/Application/RSBot.Server/RSBot.Server.csproj @@ -0,0 +1,14 @@ + + + + + + + + + Exe + net8.0 + enable + enable + + diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index 7e481529..1996d910 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Reflection; using System.Text; @@ -13,6 +13,8 @@ namespace RSBot; internal static class Program { + public static Main MainForm { get; private set; } + public static string AssemblyTitle = Assembly .GetExecutingAssembly() .GetCustomAttribute() @@ -33,6 +35,9 @@ public class CommandLineOptions [Option('p', "profile", Required = false, HelpText = "Set the profile name to use.")] public string Profile { get; set; } + + [Option("listen", Required = false, HelpText = "Enable IPC and listen on the specified pipe name.")] + public string Listen { get; set; } } private static void DisplayHelp(ParserResult result) @@ -83,11 +88,11 @@ 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); + Application.Run(MainForm); } private static void RunOptions(CommandLineOptions options) @@ -110,5 +115,10 @@ private static void RunOptions(CommandLineOptions options) ProfileManager.SelectedCharacter = character; Log.Debug($"Selected character by args: {character}"); } + + if (!string.IsNullOrEmpty(options.Listen)) + { + IpcManager.Initialize(options.Listen); + } } } diff --git a/Application/RSBot/Views/Main.cs b/Application/RSBot/Views/Main.cs index 620e46d1..d2214e9f 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 b8c3ba0d..df36eaba 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 00000000..06e00c14 --- /dev/null +++ b/Library/RSBot.Core/Components/IpcManager.cs @@ -0,0 +1,136 @@ +using RSBot.IPC; +using System; +using System.Threading.Tasks; +using RSBot.Core.Event; + +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."); + + // Register the bot with the 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..."); + // Implement reconnection logic if needed + 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) + { + // Process the command + IpcResponse response = await HandleCommand(command); + + // Send the response + 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: + // Logic to stop the bot + Kernel.Bot.Stop(); + response.Message = "Bot stopped."; + break; + + case CommandType.Start: + // Logic to start the bot + Kernel.Bot.Start(); + response.Message = "Bot started."; + break; + + case CommandType.GetInfo: + // Logic to get bot info + 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; + + // Add other command handlers here... + + default: + response.Success = false; + response.Message = $"Unknown command type: {command.CommandType}"; + break; + } + + return response; + } + } +} diff --git a/Library/RSBot.Core/RSBot.Core.csproj b/Library/RSBot.Core/RSBot.Core.csproj index 0a46a2b2..6011191b 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 00000000..92c3e563 --- /dev/null +++ b/Library/RSBot.IPC/CommandType.cs @@ -0,0 +1,22 @@ +namespace RSBot.IPC +{ + public enum CommandType + { + RegisterBot, // Added for bot registration + Stop, + Start, + GetInfo, + SetVisibility, + GoClientless, + 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 00000000..5c79417d --- /dev/null +++ b/Library/RSBot.IPC/IpcCommand.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using RSBot.IPC; + +namespace RSBot.IPC +{ + public class IpcCommand + { + public string RequestId { get; set; } // Unique identifier for the request + public CommandType CommandType { get; set; } + public string Profile { get; set; } + public string Payload { 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 00000000..e5e38950 --- /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; } // Unique identifier for the request this is a response to + 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 00000000..ab55ed56 --- /dev/null +++ b/Library/RSBot.IPC/NamedPipeClient.cs @@ -0,0 +1,107 @@ +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); // 5 second timeout + Log("Successfully connected to pipe."); + Connected?.Invoke(); + _ = ReadMessagesAsync(); // Start listening for messages + } + 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) + { + // Pipe was closed + 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 00000000..5be10fbb --- /dev/null +++ b/Library/RSBot.IPC/NamedPipeServer.cs @@ -0,0 +1,146 @@ +using System; +using System.IO.Pipes; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Concurrent; // Added for ConcurrentDictionary + +namespace RSBot.IPC +{ + public class NamedPipeServer + { + private readonly string _pipeName; + private readonly ConcurrentDictionary _clientPipesMap = new ConcurrentDictionary(); + private bool _isRunning; + + public event Action MessageReceived; // clientPipeId, message + public event Action ClientConnected; // clientPipeId + public event Action ClientDisconnected; // clientPipeId + + 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, // Max number of server instances + 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(); // Unique ID for this client connection + _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) + { + // Client disconnected + 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 00000000..76329e4a --- /dev/null +++ b/Library/RSBot.IPC/RSBot.IPC.csproj @@ -0,0 +1,11 @@ + + + net8.0 + enable + enable + + + + + + diff --git a/RSBot.sln b/RSBot.sln index 1151dfd9..b3cacd61 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 1425ee46..91a50380 100644 --- a/build.ps1 +++ b/build.ps1 @@ -23,6 +23,7 @@ if (-not (Test-Path ".\SDUI")) { taskkill /F /IM RSBot.exe taskkill /F /IM sro_client.exe +taskkill /F /IM RSBot.Server.exe if ($Clean) { Write-Output "Performing a clean build..." From fc02956db5bd623e2d8ff47155a98c325582cec8 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 02:16:20 +0300 Subject: [PATCH 3/9] Test and fix base functionality --- AGENTS.md | 2 +- Application/RSBot.Controller/Program.cs | 36 +-- .../RSBot.Controller/RSBot.Controller.csproj | 2 + Application/RSBot.Server/Program.cs | 233 ++++++++++++------ Application/RSBot.Server/RSBot.Server.csproj | 4 +- Application/RSBot/Program.cs | 2 +- Library/RSBot.Core/Components/IpcManager.cs | 8 - Library/RSBot.IPC/CommandType.cs | 2 +- Library/RSBot.IPC/IpcCommand.cs | 2 +- Library/RSBot.IPC/IpcResponse.cs | 2 +- Library/RSBot.IPC/NamedPipeClient.cs | 5 +- Library/RSBot.IPC/NamedPipeServer.cs | 13 +- build.ps1 | 13 +- vscode.code-workspace | 31 +++ 14 files changed, 233 insertions(+), 122 deletions(-) create mode 100644 vscode.code-workspace diff --git a/AGENTS.md b/AGENTS.md index c03bb043..ada7b035 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 index 54fbb41d..b57e62c8 100644 --- a/Application/RSBot.Controller/Program.cs +++ b/Application/RSBot.Controller/Program.cs @@ -18,7 +18,7 @@ public class Options [Option('d', "data", Required = false, HelpText = "The data payload for the command.")] public string Data { get; set; } - [Option("pipename", Required = false, HelpText = "The name of the pipe to connect to.", Default = "RSBotIpcServer")] + [Option('x', "pipename", Required = false, HelpText = "The name of the pipe to connect to.", Default = "RSBotIPC")] public string PipeName { get; set; } } @@ -45,7 +45,7 @@ static async Task RunOptions(Options opts) }; var pipeClient = new NamedPipeClient(opts.PipeName, Console.WriteLine); - bool responseReceived = false; + var responseTcs = new TaskCompletionSource(); pipeClient.MessageReceived += (message) => { @@ -60,40 +60,44 @@ static async Task RunOptions(Options opts) { Console.WriteLine($"Payload: {response.Payload}"); } - responseReceived = true; - pipeClient.Disconnect(); + responseTcs.TrySetResult(true); } } catch (Exception ex) { Console.WriteLine($"Error processing response: {ex.Message}"); - responseReceived = true; - + responseTcs.TrySetResult(false); } }; pipeClient.Disconnected += () => { - if (!responseReceived) + if (!responseTcs.Task.IsCompleted) { Console.WriteLine("Disconnected from server before receiving a response."); + responseTcs.TrySetResult(false); } }; await pipeClient.ConnectAsync(); + await pipeClient.SendMessageAsync(command.ToJson()); + var timeoutTask = Task.Delay(5000); + var completedTask = await Task.WhenAny(responseTcs.Task, timeoutTask); - // Wait for a response or timeout - var timeout = Task.Delay(5000); // 5 second timeout - while (!responseReceived) + if (completedTask == timeoutTask) { - if (await Task.WhenAny(timeout) == timeout) - { - Console.WriteLine("Timeout waiting for a response from the server."); - break; - } - await Task.Delay(100); + Console.WriteLine("Timeout waiting for a response from the server."); } + else if (responseTcs.Task.Result) + { + } + else + { + Console.WriteLine("Failed to receive a valid response or disconnected unexpectedly."); + } + + pipeClient.Disconnect(); } } } diff --git a/Application/RSBot.Controller/RSBot.Controller.csproj b/Application/RSBot.Controller/RSBot.Controller.csproj index 638efb85..1be63f32 100644 --- a/Application/RSBot.Controller/RSBot.Controller.csproj +++ b/Application/RSBot.Controller/RSBot.Controller.csproj @@ -9,7 +9,9 @@ Exe + ..\..\Build\ net8.0 + false enable enable diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs index 972dd80e..58db2fa7 100644 --- a/Application/RSBot.Server/Program.cs +++ b/Application/RSBot.Server/Program.cs @@ -5,44 +5,92 @@ using CommandLine; using Newtonsoft.Json; using RSBot.IPC; +using System.IO; +using System.Reflection; +using System.Text; +using Newtonsoft.Json.Linq; 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(); // Profile -> ClientPipeId - private static readonly ConcurrentDictionary _cliRequestMap = new ConcurrentDictionary(); // RequestId -> CliClientPipeId + private static readonly ConcurrentDictionary _botClientConnections = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _cliRequestMap = new ConcurrentDictionary(); public class Options { - [Option("pipename", Required = false, HelpText = "The name of the pipe to listen on.", Default = "RSBotIpcServer")] + [Option("pipename", Required = false, HelpText = "The name of the pipe to listen on.", Default = "RSBotIPC")] public string PipeName { get; set; } } - static void Main(string[] args) + private static TaskCompletionSource _serverStopped = new TaskCompletionSource(); + + static async Task Main(string[] args) { - Parser.Default.ParseArguments(args) - .WithParsed(o => + SetupLogging("Server.log"); + await Parser.Default.ParseArguments(args) + .WithParsedAsync(async o => { - RunServer(o.PipeName); + await RunServer(o.PipeName); }); } - static void RunServer(string 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 += OnMessageReceived; + _serverPipe.MessageReceived += async (clientPipeId, message) => await OnMessageReceived(clientPipeId, message); _serverPipe.Start(); Console.WriteLine($"IPC Server listening on pipe: {pipeName}"); - Console.WriteLine("Press any key to stop the server."); - Console.ReadKey(); + await _serverStopped.Task; _serverPipe.Stop(); Console.WriteLine("RSBot IPC Server Stopped."); @@ -56,16 +104,12 @@ private static void OnClientConnected(string clientPipeId) private static void OnClientDisconnected(string clientPipeId) { Console.WriteLine($"Client disconnected: {clientPipeId}"); - - // Remove from bot client connections var botEntry = _botClientConnections.FirstOrDefault(x => x.Value == clientPipeId); if (botEntry.Key != null) { _botClientConnections.TryRemove(botEntry.Key, out _); Console.WriteLine($"Bot client '{botEntry.Key}' removed."); } - - // Also check if the disconnected client has pending requests in the map var requestsToRemove = _cliRequestMap.Where(kvp => kvp.Value == clientPipeId).Select(kvp => kvp.Key).ToList(); foreach (var requestId in requestsToRemove) { @@ -74,87 +118,130 @@ private static void OnClientDisconnected(string clientPipeId) } } - private static async void OnMessageReceived(string clientPipeId, string message) + private static async Task OnMessageReceived(string clientPipeId, string message) { Console.WriteLine($"Message received from {clientPipeId}: {message}"); - - // Try to parse as IpcCommand first + JObject jsonObject; try { - IpcCommand command = IpcCommand.FromJson(message); - if (command != null) + 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 { - if (command.CommandType == CommandType.RegisterBot) + IpcCommand command = jsonObject.ToObject(); + if (command != null) { - // This is a registration command from a bot client - string profileName = command.Profile; - if (!string.IsNullOrEmpty(profileName)) + if (command.CommandType == CommandType.RegisterBot) { - _botClientConnections[profileName] = clientPipeId; - Console.WriteLine($"Bot client for profile '{profileName}' registered with pipe ID {clientPipeId}."); + 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 - { - // This is a command from a CLI client - if (!string.IsNullOrEmpty(command.RequestId)) + else { - _cliRequestMap[command.RequestId] = clientPipeId; - } + 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}"); + Console.WriteLine($"Received command '{command.CommandType}' for profile '{command.Profile}' from CLI client {clientPipeId}"); - if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) - { - Console.WriteLine($"Routing command to bot client {botClientPipeId} for profile '{command.Profile}'"); - await _serverPipe.SendMessageToClientAsync(botClientPipeId, message); - } - else - { - // Bot client not found, send error response back to CLI client - IpcResponse errorResponse = new IpcResponse + if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) { - RequestId = command.RequestId, - Success = false, - Message = $"Bot client for profile '{command.Profile}' not found.", - Payload = "" - }; - await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); - _cliRequestMap.TryRemove(command.RequestId, out _); // Clean up the request map + 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()); + _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()); + _cliRequestMap.TryRemove(command.RequestId, out _); + } } } - return; + } + catch (Exception ex) + { + Console.WriteLine($"Error processing message as IpcCommand: {ex.Message}. Message: {message}"); } } - catch (JsonException) { /* Not an IpcCommand, proceed to check if it's an IpcResponse */ } - - // Try to parse as IpcResponse - try + else if (jsonObject.ContainsKey("Success")) { - IpcResponse response = IpcResponse.FromJson(message); - if (response != null && !string.IsNullOrEmpty(response.RequestId)) + try { - // This is a response from a bot client - Console.WriteLine($"Received response for request '{response.RequestId}' from bot client {clientPipeId}"); - - if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) - { - Console.WriteLine($"Routing response back to CLI client {cliClientPipeId}"); - await _serverPipe.SendMessageToClientAsync(cliClientPipeId, message); - } - else + IpcResponse response = jsonObject.ToObject(); + if (response != null && !string.IsNullOrEmpty(response.RequestId)) { - Console.WriteLine($"Could not find originating CLI client for request ID '{response.RequestId}'."); + Console.WriteLine($"Received response for request '{response.RequestId}' from bot client {clientPipeId}"); + + if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) + { + 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}"); + } } - catch (JsonException) - { - Console.WriteLine($"Received unparseable message from {clientPipeId}: {message}"); - } - catch (Exception ex) + else { - Console.WriteLine($"Error processing message from {clientPipeId}: {ex.Message}"); + 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 index e95f17dd..b04b6481 100644 --- a/Application/RSBot.Server/RSBot.Server.csproj +++ b/Application/RSBot.Server/RSBot.Server.csproj @@ -6,8 +6,10 @@ - Exe + WinExe + ..\..\Build\ net8.0 + false enable enable diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index 1996d910..bacb88a0 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -36,7 +36,7 @@ public class CommandLineOptions [Option('p', "profile", Required = false, HelpText = "Set the profile name to use.")] public string Profile { get; set; } - [Option("listen", Required = false, HelpText = "Enable IPC and listen on the specified pipe name.")] + [Option('l', "listen", Required = false, HelpText = "Enable IPC and listen on the specified pipe name.")] public string Listen { get; set; } } diff --git a/Library/RSBot.Core/Components/IpcManager.cs b/Library/RSBot.Core/Components/IpcManager.cs index 06e00c14..868de122 100644 --- a/Library/RSBot.Core/Components/IpcManager.cs +++ b/Library/RSBot.Core/Components/IpcManager.cs @@ -28,7 +28,6 @@ private static async void OnConnected() { Log.Debug("IPC: Connected to server."); - // Register the bot with the server var profileName = ProfileManager.SelectedProfile; if (!string.IsNullOrEmpty(profileName)) { @@ -46,7 +45,6 @@ private static async void OnConnected() private static void OnDisconnected() { Log.Debug("IPC: Disconnected from server. Reconnecting..."); - // Implement reconnection logic if needed Task.Delay(5000).ContinueWith(t => _pipeClient.ConnectAsync()); } @@ -59,10 +57,8 @@ private static async void OnMessageReceived(string message) IpcCommand command = IpcCommand.FromJson(message); if (command != null) { - // Process the command IpcResponse response = await HandleCommand(command); - // Send the response if (response != null) { await _pipeClient.SendMessageAsync(response.ToJson()); @@ -87,19 +83,16 @@ private static async Task HandleCommand(IpcCommand command) switch (command.CommandType) { case CommandType.Stop: - // Logic to stop the bot Kernel.Bot.Stop(); response.Message = "Bot stopped."; break; case CommandType.Start: - // Logic to start the bot Kernel.Bot.Start(); response.Message = "Bot started."; break; case CommandType.GetInfo: - // Logic to get bot info response.Payload = new { Profile = ProfileManager.SelectedProfile, @@ -122,7 +115,6 @@ private static async Task HandleCommand(IpcCommand command) response.Message = "Switched to clientless mode."; break; - // Add other command handlers here... default: response.Success = false; diff --git a/Library/RSBot.IPC/CommandType.cs b/Library/RSBot.IPC/CommandType.cs index 92c3e563..b5adeab2 100644 --- a/Library/RSBot.IPC/CommandType.cs +++ b/Library/RSBot.IPC/CommandType.cs @@ -2,7 +2,7 @@ namespace RSBot.IPC { public enum CommandType { - RegisterBot, // Added for bot registration + RegisterBot, Stop, Start, GetInfo, diff --git a/Library/RSBot.IPC/IpcCommand.cs b/Library/RSBot.IPC/IpcCommand.cs index 5c79417d..18ffe5c6 100644 --- a/Library/RSBot.IPC/IpcCommand.cs +++ b/Library/RSBot.IPC/IpcCommand.cs @@ -5,7 +5,7 @@ namespace RSBot.IPC { public class IpcCommand { - public string RequestId { get; set; } // Unique identifier for the request + public string RequestId { get; set; } public CommandType CommandType { get; set; } public string Profile { get; set; } public string Payload { get; set; } diff --git a/Library/RSBot.IPC/IpcResponse.cs b/Library/RSBot.IPC/IpcResponse.cs index e5e38950..72a15672 100644 --- a/Library/RSBot.IPC/IpcResponse.cs +++ b/Library/RSBot.IPC/IpcResponse.cs @@ -4,7 +4,7 @@ namespace RSBot.IPC { public class IpcResponse { - public string RequestId { get; set; } // Unique identifier for the request this is a response to + public string RequestId { get; set; } public bool Success { get; set; } public string Message { get; set; } public string Payload { get; set; } diff --git a/Library/RSBot.IPC/NamedPipeClient.cs b/Library/RSBot.IPC/NamedPipeClient.cs index ab55ed56..6c95ca02 100644 --- a/Library/RSBot.IPC/NamedPipeClient.cs +++ b/Library/RSBot.IPC/NamedPipeClient.cs @@ -32,10 +32,10 @@ public async Task ConnectAsync() try { Log($"Attempting to connect to pipe {_pipeName}..."); - await _pipeClient.ConnectAsync(5000); // 5 second timeout + await _pipeClient.ConnectAsync(5000); Log("Successfully connected to pipe."); Connected?.Invoke(); - _ = ReadMessagesAsync(); // Start listening for messages + _ = ReadMessagesAsync(); } catch (TimeoutException) { @@ -78,7 +78,6 @@ private async Task ReadMessagesAsync() } else if (bytesRead == 0) { - // Pipe was closed break; } } diff --git a/Library/RSBot.IPC/NamedPipeServer.cs b/Library/RSBot.IPC/NamedPipeServer.cs index 5be10fbb..3217a2a1 100644 --- a/Library/RSBot.IPC/NamedPipeServer.cs +++ b/Library/RSBot.IPC/NamedPipeServer.cs @@ -2,7 +2,7 @@ using System.IO.Pipes; using System.Text; using System.Threading.Tasks; -using System.Collections.Concurrent; // Added for ConcurrentDictionary +using System.Collections.Concurrent; namespace RSBot.IPC { @@ -12,9 +12,9 @@ public class NamedPipeServer private readonly ConcurrentDictionary _clientPipesMap = new ConcurrentDictionary(); private bool _isRunning; - public event Action MessageReceived; // clientPipeId, message - public event Action ClientConnected; // clientPipeId - public event Action ClientDisconnected; // clientPipeId + public event Func MessageReceived; + public event Action ClientConnected; + public event Action ClientDisconnected; public NamedPipeServer(string pipeName) { @@ -48,7 +48,7 @@ private async Task ListenForConnections() NamedPipeServerStream pipeServer = new NamedPipeServerStream( _pipeName, PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, // Max number of server instances + NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); Console.WriteLine("Waiting for a client to connect..."); @@ -57,7 +57,7 @@ private async Task ListenForConnections() if (_isRunning) { Console.WriteLine("Client connected."); - string clientPipeId = Guid.NewGuid().ToString(); // Unique ID for this client connection + string clientPipeId = Guid.NewGuid().ToString(); _clientPipesMap.TryAdd(clientPipeId, pipeServer); ClientConnected?.Invoke(clientPipeId); _ = HandleClientConnection(pipeServer, clientPipeId); @@ -90,7 +90,6 @@ private async Task HandleClientConnection(NamedPipeServerStream pipeServer, stri } else if (bytesRead == 0) { - // Client disconnected break; } } diff --git a/build.ps1 b/build.ps1 index 91a50380..247ef543 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,14 +1,8 @@ -# Usage: -# `build.ps1 -# -Clean[False, optional] -# -DoNotStart[False, optional] -# -Configuration[Debug, optional]` - param( [string]$Configuration = "Debug", [switch]$Clean, [switch]$CleanRepo, - [switch]$DoNotStart + [switch]$Start ) if ($CleanRepo) { @@ -21,9 +15,10 @@ if (-not (Test-Path ".\SDUI")) { 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..." @@ -44,7 +39,7 @@ if ($Clean) { Remove-Item -Recurse -Force ".\temp" -ErrorAction SilentlyContinue > $null } -if (!$DoNotStart) { +if ($Start) { Write-Output "Starting RSBot..." & ".\Build\RSBot.exe" } diff --git a/vscode.code-workspace b/vscode.code-workspace new file mode 100644 index 00000000..e3ef86f8 --- /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 + } + } +} From a9584e56078e478f4410e5aa5187b2f67e5c2777 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 13:04:32 +0300 Subject: [PATCH 4/9] feat: Implement IPC commands for client management and batch operations This commit introduces several new functionalities and improvements to the Inter-Process Communication (IPC) system: - **Client Management Commands:** - Implemented `LaunchClient` command: Allows launching a game client instance remotely via IPC. - Implemented `KillClient` command: Enables terminating a running game client instance remotely via IPC. - Implemented `SetClientVisibility` command: Provides the ability to show or hide the game client window remotely via IPC. - **Batch Command Operations:** - Added `--all` option to `RSBot.Controller`: Users can now send a single command to all connected bot instances listening on a specific IPC server. - Enhanced `RSBot.Server` routing: The server now correctly broadcasts commands to all bot clients when the `TargetAllProfiles` flag is set in an `IpcCommand`. - Improved `RSBot.Controller` response handling: The controller now collects and displays all responses from multiple bot instances when a batch command is executed, rather than just the first response. - **IPC Structure Updates:** - Added `TargetAllProfiles` boolean property to `IpcCommand` to facilitate batch command routing. These changes significantly enhance the control and flexibility of managing multiple RSBot instances via the command-line interface. --- Application/RSBot.Controller/Program.cs | 105 ++++++++++++------ Application/RSBot.Server/Program.cs | 117 ++++++++++++++------ Library/RSBot.Core/Components/IpcManager.cs | 17 +++ Library/RSBot.IPC/CommandType.cs | 1 + Library/RSBot.IPC/IpcCommand.cs | 1 + 5 files changed, 175 insertions(+), 66 deletions(-) diff --git a/Application/RSBot.Controller/Program.cs b/Application/RSBot.Controller/Program.cs index b57e62c8..c371cf4a 100644 --- a/Application/RSBot.Controller/Program.cs +++ b/Application/RSBot.Controller/Program.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading.Tasks; using CommandLine; using RSBot.IPC; @@ -9,7 +11,7 @@ internal class Program { public class Options { - [Option('p', "profile", Required = true, HelpText = "The profile name to target.")] + [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.")] @@ -20,32 +22,47 @@ public class Options [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(RunOptions); - } + .WithParsedAsync(async opts => + { + if (!opts.AllProfiles && string.IsNullOrEmpty(opts.Profile)) + { + Console.WriteLine("Error: Either --profile or --all must be specified."); + return; + } - static async Task RunOptions(Options opts) - { - if (!Enum.TryParse(opts.Command, true, out var commandType)) - { - Console.WriteLine($"Error: Invalid command '{opts.Command}'."); - 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 - }; + var command = new IpcCommand + { + RequestId = Guid.NewGuid().ToString(), + CommandType = commandType, + Profile = opts.Profile, + Payload = opts.Data, + TargetAllProfiles = opts.AllProfiles + }; + + await ExecuteCommand(command, opts.PipeName); + }); + } - var pipeClient = new NamedPipeClient(opts.PipeName, Console.WriteLine); - var responseTcs = new TaskCompletionSource(); + 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) => { @@ -54,48 +71,70 @@ static async Task RunOptions(Options opts) var response = IpcResponse.FromJson(message); if (response != null && response.RequestId == command.RequestId) { - Console.WriteLine($"Success: {response.Success}"); - Console.WriteLine($"Message: {response.Message}"); - if (!string.IsNullOrEmpty(response.Payload)) + collectedResponses.Add(response); + if (!command.TargetAllProfiles) { - Console.WriteLine($"Payload: {response.Payload}"); + singleResponseTcs.TrySetResult(true); } - responseTcs.TrySetResult(true); } } catch (Exception ex) { Console.WriteLine($"Error processing response: {ex.Message}"); - responseTcs.TrySetResult(false); + if (!command.TargetAllProfiles) + { + singleResponseTcs.TrySetResult(false); + } } }; pipeClient.Disconnected += () => { - if (!responseTcs.Task.IsCompleted) + if (!command.TargetAllProfiles && !singleResponseTcs.Task.IsCompleted) { Console.WriteLine("Disconnected from server before receiving a response."); - responseTcs.TrySetResult(false); + singleResponseTcs.TrySetResult(false); } }; await pipeClient.ConnectAsync(); await pipeClient.SendMessageAsync(command.ToJson()); - var timeoutTask = Task.Delay(5000); - var completedTask = await Task.WhenAny(responseTcs.Task, timeoutTask); - if (completedTask == timeoutTask) + Task completedTask; + if (command.TargetAllProfiles) { - Console.WriteLine("Timeout waiting for a response from the server."); + // 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 if (responseTcs.Task.Result) + else { + // For single commands, wait for a single response or timeout + completedTask = await Task.WhenAny(singleResponseTcs.Task, Task.Delay(BATCH_COMMAND_TIMEOUT_MS)); } - else + + 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.Server/Program.cs b/Application/RSBot.Server/Program.cs index 58db2fa7..5d93ec4f 100644 --- a/Application/RSBot.Server/Program.cs +++ b/Application/RSBot.Server/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommandLine; @@ -154,48 +155,95 @@ private static async Task OnMessageReceived(string clientPipeId, string message) } else { - if (!string.IsNullOrEmpty(command.RequestId)) + lock (_cliRequestMap) { - _cliRequestMap[command.RequestId] = clientPipeId; - Console.WriteLine($"CLI client request '{command.RequestId}' mapped to {clientPipeId}."); + 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 (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) + if (command.TargetAllProfiles) { - Console.WriteLine($"Routing command '{command.CommandType}' to bot client {botClientPipeId} for profile '{command.Profile}'."); - try + Console.WriteLine($"Broadcasting command '{command.CommandType}' to all connected bot clients."); + if (_botClientConnections.Any()) { - await _serverPipe.SendMessageToClientAsync(botClientPipeId, message); - Console.WriteLine($"Command '{command.CommandType}' successfully routed to bot client {botClientPipeId}."); + 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}"); + } + } } - catch (Exception ex) + else { - Console.WriteLine($"Error routing command '{command.CommandType}' to bot client {botClientPipeId}: {ex.Message}"); + 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 = $"Error routing command to bot client for profile '{command.Profile}': {ex.Message}", + Message = "No bot clients connected.", Payload = "" }; await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); - _cliRequestMap.TryRemove(command.RequestId, out _); + 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 + if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) { - RequestId = command.RequestId, - Success = false, - Message = $"Bot client for profile '{command.Profile}' not found.", - Payload = "" - }; - await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); - _cliRequestMap.TryRemove(command.RequestId, out _); + 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 _); + } + } } } } @@ -214,24 +262,27 @@ private static async Task OnMessageReceived(string clientPipeId, string message) { Console.WriteLine($"Received response for request '{response.RequestId}' from bot client {clientPipeId}"); - if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) + lock (_cliRequestMap) { - Console.WriteLine($"Routing response for request '{response.RequestId}' back to CLI client {cliClientPipeId}."); - try + if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) { - 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}."); + 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}"); + } } - catch (Exception ex) + else { - Console.WriteLine($"Error sending response to CLI client {cliClientPipeId}: {ex.Message}"); + Console.WriteLine($"Could not find originating CLI client for request ID '{response.RequestId}'. Message: {message}"); } } - else - { - Console.WriteLine($"Could not find originating CLI client for request ID '{response.RequestId}'. Message: {message}"); - } } } catch (Exception ex) diff --git a/Library/RSBot.Core/Components/IpcManager.cs b/Library/RSBot.Core/Components/IpcManager.cs index 868de122..7195e290 100644 --- a/Library/RSBot.Core/Components/IpcManager.cs +++ b/Library/RSBot.Core/Components/IpcManager.cs @@ -115,6 +115,23 @@ private static async Task HandleCommand(IpcCommand command) 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; diff --git a/Library/RSBot.IPC/CommandType.cs b/Library/RSBot.IPC/CommandType.cs index b5adeab2..b8cd8abc 100644 --- a/Library/RSBot.IPC/CommandType.cs +++ b/Library/RSBot.IPC/CommandType.cs @@ -8,6 +8,7 @@ public enum CommandType GetInfo, SetVisibility, GoClientless, + SetClientVisibility, LaunchClient, KillClient, SetOptions, diff --git a/Library/RSBot.IPC/IpcCommand.cs b/Library/RSBot.IPC/IpcCommand.cs index 18ffe5c6..a8aa3d91 100644 --- a/Library/RSBot.IPC/IpcCommand.cs +++ b/Library/RSBot.IPC/IpcCommand.cs @@ -9,6 +9,7 @@ public class IpcCommand public CommandType CommandType { get; set; } public string Profile { get; set; } public string Payload { get; set; } + public bool TargetAllProfiles { get; set; } public string ToJson() { From 0d81e9ae5a8785f3ef264e60035f86613c2ffdcf Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 15:51:38 +0300 Subject: [PATCH 5/9] CSharpier --- Application/RSBot.Controller/Program.cs | 28 ++++- Application/RSBot.Server/Program.cs | 122 ++++++++++++++------ Library/RSBot.Core/Components/IpcManager.cs | 9 +- Library/RSBot.Core/Config/GlobalConfig.cs | 5 +- Library/RSBot.IPC/CommandType.cs | 2 +- Library/RSBot.IPC/NamedPipeServer.cs | 8 +- 6 files changed, 120 insertions(+), 54 deletions(-) diff --git a/Application/RSBot.Controller/Program.cs b/Application/RSBot.Controller/Program.cs index c371cf4a..b516e2f6 100644 --- a/Application/RSBot.Controller/Program.cs +++ b/Application/RSBot.Controller/Program.cs @@ -11,7 +11,12 @@ internal class Program { public class Options { - [Option('p', "profile", Required = false, HelpText = "The profile name to target. Required unless --all is used.")] + [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.")] @@ -20,16 +25,29 @@ public class Options [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")] + [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)] + [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) + await Parser + .Default.ParseArguments(args) .WithParsedAsync(async opts => { if (!opts.AllProfiles && string.IsNullOrEmpty(opts.Profile)) @@ -50,7 +68,7 @@ await Parser.Default.ParseArguments(args) CommandType = commandType, Profile = opts.Profile, Payload = opts.Data, - TargetAllProfiles = opts.AllProfiles + TargetAllProfiles = opts.AllProfiles, }; await ExecuteCommand(command, opts.PipeName); diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs index 5d93ec4f..e09d7b30 100644 --- a/Application/RSBot.Server/Program.cs +++ b/Application/RSBot.Server/Program.cs @@ -1,15 +1,15 @@ 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 RSBot.IPC; -using System.IO; -using System.Reflection; -using System.Text; using Newtonsoft.Json.Linq; +using RSBot.IPC; namespace RSBot.Server { @@ -43,12 +43,19 @@ public override void WriteLine(string value) } private static NamedPipeServer _serverPipe; - private static readonly ConcurrentDictionary _botClientConnections = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary _cliRequestMap = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _botClientConnections = + new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _cliRequestMap = + new ConcurrentDictionary(); public class Options { - [Option("pipename", Required = false, HelpText = "The name of the pipe to listen on.", Default = "RSBotIPC")] + [Option( + "pipename", + Required = false, + HelpText = "The name of the pipe to listen on.", + Default = "RSBotIPC" + )] public string PipeName { get; set; } } @@ -57,11 +64,12 @@ public class Options static async Task Main(string[] args) { SetupLogging("Server.log"); - await Parser.Default.ParseArguments(args) - .WithParsedAsync(async o => - { - await RunServer(o.PipeName); - }); + await Parser + .Default.ParseArguments(args) + .WithParsedAsync(async o => + { + await RunServer(o.PipeName); + }); } private static void SetupLogging(string logFileName) @@ -86,7 +94,8 @@ static async Task RunServer(string pipeName) _serverPipe = new NamedPipeServer(pipeName); _serverPipe.ClientConnected += OnClientConnected; _serverPipe.ClientDisconnected += OnClientDisconnected; - _serverPipe.MessageReceived += async (clientPipeId, message) => await OnMessageReceived(clientPipeId, message); + _serverPipe.MessageReceived += async (clientPipeId, message) => + await OnMessageReceived(clientPipeId, message); _serverPipe.Start(); @@ -111,7 +120,10 @@ private static void OnClientDisconnected(string clientPipeId) _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(); + var requestsToRemove = _cliRequestMap + .Where(kvp => kvp.Value == clientPipeId) + .Select(kvp => kvp.Key) + .ToList(); foreach (var requestId in requestsToRemove) { _cliRequestMap.TryRemove(requestId, out _); @@ -146,11 +158,15 @@ private static async Task OnMessageReceived(string clientPipeId, string message) if (!string.IsNullOrEmpty(profileName)) { _botClientConnections[profileName] = clientPipeId; - Console.WriteLine($"Bot client for profile '{profileName}' registered with pipe ID {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."); + Console.WriteLine( + $"Received RegisterBot command with empty profile from {clientPipeId}. Ignoring." + ); } } else @@ -160,15 +176,21 @@ private static async Task OnMessageReceived(string clientPipeId, string message) if (!string.IsNullOrEmpty(command.RequestId)) { _cliRequestMap[command.RequestId] = clientPipeId; - Console.WriteLine($"CLI client request '{command.RequestId}' mapped to {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}"); + 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."); + Console.WriteLine( + $"Broadcasting command '{command.CommandType}' to all connected bot clients." + ); if (_botClientConnections.Any()) { foreach (var botPipeId in _botClientConnections.Values) @@ -176,23 +198,29 @@ private static async Task OnMessageReceived(string clientPipeId, string message) try { await _serverPipe.SendMessageToClientAsync(botPipeId, message); - Console.WriteLine($"Command '{command.CommandType}' successfully broadcasted to bot client {botPipeId}."); + 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}"); + 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."); + 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 = "" + Payload = "", }; await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); lock (_cliRequestMap) @@ -205,23 +233,33 @@ private static async Task OnMessageReceived(string clientPipeId, string message) { if (_botClientConnections.TryGetValue(command.Profile, out string botClientPipeId)) { - Console.WriteLine($"Routing command '{command.CommandType}' to bot client {botClientPipeId} for profile '{command.Profile}'."); + 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}."); + 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}"); + 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 = "" + Message = + $"Error routing command to bot client for profile '{command.Profile}': {ex.Message}", + Payload = "", }; - await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); + await _serverPipe.SendMessageToClientAsync( + clientPipeId, + errorResponse.ToJson() + ); lock (_cliRequestMap) { _cliRequestMap.TryRemove(command.RequestId, out _); @@ -230,13 +268,15 @@ private static async Task OnMessageReceived(string clientPipeId, string message) } else { - Console.WriteLine($"Bot client for profile '{command.Profile}' not found. Sending error response to CLI client {clientPipeId}."); + 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 = "" + Payload = "", }; await _serverPipe.SendMessageToClientAsync(clientPipeId, errorResponse.ToJson()); lock (_cliRequestMap) @@ -260,27 +300,37 @@ private static async Task OnMessageReceived(string clientPipeId, string message) IpcResponse response = jsonObject.ToObject(); if (response != null && !string.IsNullOrEmpty(response.RequestId)) { - Console.WriteLine($"Received response for request '{response.RequestId}' from bot client {clientPipeId}"); + Console.WriteLine( + $"Received response for request '{response.RequestId}' from bot client {clientPipeId}" + ); lock (_cliRequestMap) { if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) { - Console.WriteLine($"Routing response for request '{response.RequestId}' back to CLI client {cliClientPipeId}."); + 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}"); + 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}"); + 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}"); + Console.WriteLine( + $"Could not find originating CLI client for request ID '{response.RequestId}'. Message: {message}" + ); } } } diff --git a/Library/RSBot.Core/Components/IpcManager.cs b/Library/RSBot.Core/Components/IpcManager.cs index 7195e290..ac0c415b 100644 --- a/Library/RSBot.Core/Components/IpcManager.cs +++ b/Library/RSBot.Core/Components/IpcManager.cs @@ -1,7 +1,7 @@ -using RSBot.IPC; using System; using System.Threading.Tasks; using RSBot.Core.Event; +using RSBot.IPC; namespace RSBot.Core.Components { @@ -35,7 +35,7 @@ private static async void OnConnected() { CommandType = CommandType.RegisterBot, Profile = profileName, - RequestId = Guid.NewGuid().ToString() + RequestId = Guid.NewGuid().ToString(), }; await _pipeClient.SendMessageAsync(command.ToJson()); Log.Debug($"IPC: Sent registration for profile '{profileName}'."); @@ -77,7 +77,7 @@ private static async Task HandleCommand(IpcCommand command) { RequestId = command.RequestId, Success = true, - Message = "Command processed." + Message = "Command processed.", }; switch (command.CommandType) @@ -100,7 +100,7 @@ private static async Task HandleCommand(IpcCommand command) Location = Game.Player?.Position.ToString(), Uptime = Kernel.Bot.Uptime, Botbase = Kernel.Bot.Botbase?.Name, - ClientVisible = !Game.Clientless + ClientVisible = !Game.Clientless, }.ToString(); break; @@ -132,7 +132,6 @@ private static async Task HandleCommand(IpcCommand command) response.Message = "Client killed."; break; - default: response.Success = false; response.Message = $"Unknown command type: {command.CommandType}"; diff --git a/Library/RSBot.Core/Config/GlobalConfig.cs b/Library/RSBot.Core/Config/GlobalConfig.cs index d50ed972..1613ab16 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.IPC/CommandType.cs b/Library/RSBot.IPC/CommandType.cs index b8cd8abc..156adfc7 100644 --- a/Library/RSBot.IPC/CommandType.cs +++ b/Library/RSBot.IPC/CommandType.cs @@ -18,6 +18,6 @@ public enum CommandType Move, SpecifyTrainingArea, SelectBotbase, - ReturnToTown + ReturnToTown, } } diff --git a/Library/RSBot.IPC/NamedPipeServer.cs b/Library/RSBot.IPC/NamedPipeServer.cs index 3217a2a1..06d7575f 100644 --- a/Library/RSBot.IPC/NamedPipeServer.cs +++ b/Library/RSBot.IPC/NamedPipeServer.cs @@ -1,15 +1,16 @@ using System; +using System.Collections.Concurrent; using System.IO.Pipes; using System.Text; using System.Threading.Tasks; -using System.Collections.Concurrent; namespace RSBot.IPC { public class NamedPipeServer { private readonly string _pipeName; - private readonly ConcurrentDictionary _clientPipesMap = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _clientPipesMap = + new ConcurrentDictionary(); private bool _isRunning; public event Func MessageReceived; @@ -50,7 +51,8 @@ private async Task ListenForConnections() PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, - PipeOptions.Asynchronous); + PipeOptions.Asynchronous + ); Console.WriteLine("Waiting for a client to connect..."); await pipeServer.WaitForConnectionAsync(); From 86c3bf9359baf48c3beef3cd631a8ebce2e07014 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 17:59:11 +0300 Subject: [PATCH 6/9] feat: Add an option to create an autologin entry fix a build error on server --- Application/RSBot.Server/Program.cs | 44 ++++++----- Application/RSBot/Program.cs | 77 +++++++++++++++++++- Plugins/RSBot.General/Components/Accounts.cs | 2 +- Plugins/RSBot.General/Models/Account.cs | 2 +- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs index e09d7b30..88ee2794 100644 --- a/Application/RSBot.Server/Program.cs +++ b/Application/RSBot.Server/Program.cs @@ -304,35 +304,43 @@ await _serverPipe.SendMessageToClientAsync( $"Received response for request '{response.RequestId}' from bot client {clientPipeId}" ); + string cliClientPipeId = null; + bool foundClient = false; + lock (_cliRequestMap) { - if (_cliRequestMap.TryRemove(response.RequestId, out string cliClientPipeId)) + 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( - $"Routing response for request '{response.RequestId}' back to CLI client {cliClientPipeId}." + $"Attempting to send response to CLI client {cliClientPipeId}: {message}" ); - 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}" - ); - } + await _serverPipe.SendMessageToClientAsync(cliClientPipeId, message); + Console.WriteLine($"Response successfully sent to CLI client {cliClientPipeId}."); } - else + catch (Exception ex) { Console.WriteLine( - $"Could not find originating CLI client for request ID '{response.RequestId}'. Message: {message}" + $"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) diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index bacb88a0..427df917 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text; @@ -7,6 +8,8 @@ using CommandLine.Text; using RSBot.Core; using RSBot.Core.Components; +using RSBot.General.Components; +using RSBot.General.Models; using RSBot.Views; namespace RSBot; @@ -38,8 +41,25 @@ public class CommandLineOptions [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; } + } private static void DisplayHelp(ParserResult result) { var helpText = HelpText.AutoBuild( @@ -116,6 +136,61 @@ private static void RunOptions(CommandLineOptions options) 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); diff --git a/Plugins/RSBot.General/Components/Accounts.cs b/Plugins/RSBot.General/Components/Accounts.cs index eb9bce32..88fae275 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 de4b3e91..a2531d43 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. From 9c3aeb26930ba80d0e54a24db83384ae0c0008f2 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 18:22:50 +0300 Subject: [PATCH 7/9] feat: Add an option to select an autologin --- Application/RSBot/Program.cs | 17 +++++++++++++++++ Application/RSBot/RSBot.csproj | 1 + 2 files changed, 18 insertions(+) diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index 427df917..b9d88bb0 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -17,6 +17,7 @@ namespace RSBot; internal static class Program { public static Main MainForm { get; private set; } + private static string _commandLineSelectAutologinUsername; public static string AssemblyTitle = Assembly .GetExecutingAssembly() @@ -59,6 +60,9 @@ public class CommandLineOptions [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) { @@ -112,6 +116,13 @@ private static void Main(string[] args) SplashScreen splashScreen = new SplashScreen(MainForm); splashScreen.ShowDialog(); + 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); } @@ -195,5 +206,11 @@ private static void RunOptions(CommandLineOptions options) { 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 fea695e8..ae88de61 100644 --- a/Application/RSBot/RSBot.csproj +++ b/Application/RSBot/RSBot.csproj @@ -27,6 +27,7 @@ True + From 34edd07af700456f9bf5ac787600685be68ab778 Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 18:23:47 +0300 Subject: [PATCH 8/9] CSharpier --- Application/RSBot/Program.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Application/RSBot/Program.cs b/Application/RSBot/Program.cs index b9d88bb0..f56576e3 100644 --- a/Application/RSBot/Program.cs +++ b/Application/RSBot/Program.cs @@ -57,13 +57,19 @@ public class CommandLineOptions [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.")] + [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) { var helpText = HelpText.AutoBuild( @@ -176,7 +182,8 @@ private static void RunOptions(CommandLineOptions options) } } // Default to Joymax if no provider name is specified, matching UI default behavior. - if (channel == 0) channel = 1; + if (channel == 0) + channel = 1; var newAccount = new Account { @@ -185,7 +192,7 @@ private static void RunOptions(CommandLineOptions options) SecondaryPassword = options.SecondaryPassword, Servername = options.Server, Channel = channel, - Characters = new List() // Initialize empty character list + Characters = new List(), // Initialize empty character list }; // Check if an account with the same username already exists From ae84e273e10c3a979d7952a4b1cf00058232609b Mon Sep 17 00:00:00 2001 From: Egezenn Date: Fri, 28 Nov 2025 19:22:41 +0300 Subject: [PATCH 9/9] Add alias --- Application/RSBot.Server/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Application/RSBot.Server/Program.cs b/Application/RSBot.Server/Program.cs index 88ee2794..8867d083 100644 --- a/Application/RSBot.Server/Program.cs +++ b/Application/RSBot.Server/Program.cs @@ -51,6 +51,7 @@ public override void WriteLine(string value) public class Options { [Option( + 'x', "pipename", Required = false, HelpText = "The name of the pipe to listen on.",