diff --git a/Application/OasisBot/Program.cs b/Application/OasisBot/Program.cs index d1654beb..669e8f99 100644 --- a/Application/OasisBot/Program.cs +++ b/Application/OasisBot/Program.cs @@ -42,6 +42,9 @@ public class CommandLineOptions [Option('p', "profile", Required = false, HelpText = "Set the profile name to use.")] public string Profile { get; set; } + [Option('a', "account", Required = false, HelpText = "Set the account name to use.")] + public string Account { get; set; } + [Option("launch-client", Required = false, HelpText = "Start with client")] public bool LaunchClient { get; set; } @@ -50,25 +53,43 @@ public class CommandLineOptions [Option("headless", Required = false, HelpText = "Start the bot without graphical user interface")] public bool Headless { get; set; } + + [Option('h', "help", Required = false, HelpText = "Show help message.")] + public bool Help { get; set; } } private static void DisplayHelp(ParserResult result) { - var helpText = HelpText.AutoBuild( - result, - h => - { - h.AdditionalNewLineAfterOption = false; - h.AddDashesToOption = true; - return HelpText.DefaultParsingErrorsHandler(result, h); - } + var helpText = new HelpText + { + Heading = new HeadingInfo(AssemblyTitle, AssemblyVersion), + AdditionalNewLineAfterOption = false, + AddDashesToOption = true, + }; + helpText.AddPreOptionsLine( + "Copyright (C) 2017-2021 ngoedde; 2021-2026 RSBot Team; 2026 Silkroad Developer Community" + ); + helpText.AddOptions(result); + + var lines = helpText.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var filteredLines = lines.Where(line => + !line.TrimStart().StartsWith("--help") && !line.TrimStart().StartsWith("--version") ); - Console.WriteLine(helpText); + + Console.WriteLine(string.Join(Environment.NewLine, filteredLines)); } [STAThread] private static void Main(string[] args) { + // 1. Initialize configuration and run migrations as early as possible + RSBot.Core.Config.Initialize(); + if (RSBot.Core.Config.MigrationTriggered) + { + System.Diagnostics.Process.Start(Environment.ProcessPath, args); + Environment.Exit(0); + } + var parser = new Parser(with => with.HelpWriter = null); var parserResult = parser.ParseArguments(args); @@ -77,16 +98,19 @@ private static void Main(string[] args) parserResult .WithParsed(options => { + if (options.Help) + { + DisplayHelp(parserResult); + Environment.Exit(0); + } + RunOptions(options); isHeadless = options.Headless; }) .WithNotParsed(errs => { DisplayHelp(parserResult); - var isHelp = errs.Any(e => - e.Tag == ErrorType.HelpRequestedError || e.Tag == ErrorType.VersionRequestedError - ); - Environment.Exit(isHelp ? 0 : 1); + Environment.Exit(1); }); //CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; @@ -174,13 +198,20 @@ private static void RunOptions(CommandLineOptions options) ProfileManager.IsProfileLoadedByArgs = true; Log.Debug($"Selected profile by args: {profile}"); - } - if (!string.IsNullOrEmpty(options.Character)) - { - var character = options.Character; - ProfileManager.SelectedCharacter = character; - Log.Debug($"Selected character by args: {character}"); + if (!string.IsNullOrEmpty(options.Account)) + { + var account = options.Account; + ProfileManager.SelectedAccount = account; + Log.Debug($"Selected account by args: {account}"); + + if (!string.IsNullOrEmpty(options.Character)) + { + var character = options.Character; + ProfileManager.SelectedCharacter = character; + Log.Debug($"Selected character by args: {character}"); + } + } } } } diff --git a/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.Designer.cs b/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.Designer.cs index 488470d9..0ad52455 100644 --- a/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.Designer.cs +++ b/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.Designer.cs @@ -31,7 +31,6 @@ private void InitializeComponent() comboProfiles = new SDUI.Controls.ComboBox(); label1 = new SDUI.Controls.Label(); btnOK = new SDUI.Controls.Button(); - checkSaveSelection = new SDUI.Controls.CheckBox(); buttonCreateProfile = new SDUI.Controls.Button(); buttonDeleteProfile = new SDUI.Controls.Button(); SuspendLayout(); @@ -85,23 +84,6 @@ private void InitializeComponent() btnOK.Text = "CONTINUE"; btnOK.UseVisualStyleBackColor = true; // - // checkSaveSelection - // - checkSaveSelection.AutoSize = true; - checkSaveSelection.BackColor = System.Drawing.Color.Transparent; - checkSaveSelection.Depth = 0; - checkSaveSelection.Font = new System.Drawing.Font("Segoe UI", 9F); - checkSaveSelection.Location = new System.Drawing.Point(143, 57); - checkSaveSelection.Margin = new System.Windows.Forms.Padding(0); - checkSaveSelection.MouseLocation = new System.Drawing.Point(-1, -1); - checkSaveSelection.Name = "checkSaveSelection"; - checkSaveSelection.Ripple = true; - checkSaveSelection.Size = new System.Drawing.Size(129, 30); - checkSaveSelection.TabIndex = 6; - checkSaveSelection.Text = "Save selection"; - checkSaveSelection.UseVisualStyleBackColor = false; - checkSaveSelection.CheckedChanged += checkSaveSelection_CheckedChanged; - // // buttonCreateProfile // buttonCreateProfile.Color = System.Drawing.Color.Green; @@ -143,7 +125,6 @@ private void InitializeComponent() Controls.Add(btnOK); Controls.Add(buttonDeleteProfile); Controls.Add(buttonCreateProfile); - Controls.Add(checkSaveSelection); Controls.Add(label1); Controls.Add(comboProfiles); DwmMargin = -1; @@ -162,7 +143,6 @@ private void InitializeComponent() private SDUI.Controls.ComboBox comboProfiles; private SDUI.Controls.Label label1; private SDUI.Controls.Button btnOK; - private SDUI.Controls.CheckBox checkSaveSelection; private SDUI.Controls.Button buttonCreateProfile; private SDUI.Controls.Button buttonDeleteProfile; } diff --git a/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.cs b/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.cs index 80241986..137bce8f 100644 --- a/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.cs +++ b/Application/OasisBot/Views/Dialog/ProfileSelectionDialog.cs @@ -15,7 +15,6 @@ public ProfileSelectionDialog() InitializeComponent(); LoadProfiles(); - checkSaveSelection.Checked = !ProfileManager.ShowProfileDialog; BackColor = ColorScheme.BackColor; } @@ -70,8 +69,8 @@ private string CreateNewProfile() return string.Empty; } - string[] reservedNames = { "Profiles", "Default", "Settings" }; - if (reservedNames.Any(n => n.Equals(profile, StringComparison.InvariantCultureIgnoreCase))) + string[] reservedNames = { "Settings", "Logs" }; + if (reservedNames.Any(n => n.Equals(profile, StringComparison.OrdinalIgnoreCase))) { MessageBox.Show( $"The name '{profile}' is reserved and cannot be used!", @@ -101,26 +100,8 @@ private void comboProfiles_SelectedIndexChanged(object sender, EventArgs e) SelectedProfile = (string)comboProfiles.SelectedItem; } - private void checkSaveSelection_CheckedChanged(object sender, EventArgs e) - { - ProfileManager.ShowProfileDialog = !checkSaveSelection.Checked; - } - private void buttonDeleteProfile_Click(object sender, EventArgs e) { - var selectedProfile = (string)comboProfiles.SelectedItem; - if (selectedProfile == "Default") //Default - { - MessageBox.Show( - "You can not delete the default profile!", - "Default profile", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ); - - return; - } - if (ProfileManager.SelectedProfile == (string)comboProfiles.SelectedItem) //Active profile? { MessageBox.Show( diff --git a/Application/OasisBot/Views/Main.cs b/Application/OasisBot/Views/Main.cs index e89cad07..11278f8d 100644 --- a/Application/OasisBot/Views/Main.cs +++ b/Application/OasisBot/Views/Main.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Drawing; +using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -33,6 +35,7 @@ public partial class Main : UIWindow private readonly Dictionary _pluginWindows = new(8); private bool _isWindowLoaded; private SDUI.Controls.Button btnStartSetArea; + private SDUI.Controls.Button btnDbgZip; #endregion Members @@ -561,6 +564,153 @@ private void InitializeCustomButtons() btnStartSetArea.Click += btnStartSetArea_Click; bottomPanel.Controls.Add(btnStartSetArea); + + btnDbgZip = new SDUI.Controls.Button + { + Anchor = AnchorStyles.Top | AnchorStyles.Right, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + BackColor = Color.FromArgb(90, 75, 215), + Color = Color.FromArgb(90, 75, 215), + Font = btnSave.Font, + ForeColor = Color.White, + Size = new Size((int)(btnSave.Width * 1.0), btnSave.Height), + TabIndex = 3, + TabStop = false, + Tag = "private", + Text = "DBG ZIP", + UseVisualStyleBackColor = false, + }; + + btnDbgZip.Location = new Point(btnStartSetArea.Left - btnDbgZip.Width - gap, btnStartSetArea.Top); + btnDbgZip.Click += btnDbgZip_Click; + bottomPanel.Controls.Add(btnDbgZip); + } + + /// + /// Handles the Click event of the btnDbgZip control. + /// + private void btnDbgZip_Click(object sender, EventArgs e) + { + try + { + var userDirectory = Path.Combine(Kernel.BasePath, "User"); + var logsDirectory = Path.Combine(userDirectory, "Logs"); + + if (!Directory.Exists(logsDirectory)) + Directory.CreateDirectory(logsDirectory); + + var zipFileName = $"{DateTime.Now:yyyy-MM-dd_HH_mm_ss}.zip"; + var zipFilePath = Path.Combine(logsDirectory, zipFileName); + + using (var zipStream = new FileStream(zipFilePath, FileMode.Create)) + using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + var files = Directory.GetFiles(userDirectory, "*", SearchOption.AllDirectories); + foreach (var file in files) + { + // Ignore any zip files and autologin data files + if ( + file.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) + || file.EndsWith("autologin.data", StringComparison.OrdinalIgnoreCase) + ) + continue; + + var relativePath = Path.GetRelativePath(userDirectory, file); + try + { + var entry = archive.CreateEntry(relativePath); + using (var entryStream = entry.Open()) + { + if (file.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + try + { + var json = File.ReadAllText(file); + var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true }; + var dict = System.Text.Json.JsonSerializer.Deserialize< + Dictionary + >(json); + if (dict != null) + { + var keysToRemove = dict + .Keys.Where(k => k.Equals("Accounts", StringComparison.OrdinalIgnoreCase)) + .ToList(); + foreach (var key in keysToRemove) + { + dict.Remove(key); + } + + var cleanJson = System.Text.Json.JsonSerializer.Serialize(dict, options); + var bytes = System.Text.Encoding.UTF8.GetBytes(cleanJson); + entryStream.Write(bytes, 0, bytes.Length); + } + else + { + using ( + var fileStream = new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ) + ) + { + fileStream.CopyTo(entryStream); + } + } + } + catch + { + using ( + var fileStream = new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ) + ) + { + fileStream.CopyTo(entryStream); + } + } + } + else + { + using ( + var fileStream = new FileStream( + file, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ) + ) + { + fileStream.CopyTo(entryStream); + } + } + } + } + catch (Exception ex) + { + Log.Debug($"Failed to add file {relativePath} to zip: {ex.Message}"); + } + } + } + + if (File.Exists(zipFilePath)) + { + Process.Start("explorer.exe", $"/select,\"{zipFilePath}\""); + } + } + catch (Exception ex) + { + MessageBox.Show( + $"Failed to create debug ZIP: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } } /// @@ -589,7 +739,7 @@ private void btnStartSetArea_Click(object sender, EventArgs e) if (!Kernel.Bot.Running) { var pos = Game.Player.Position; - PlayerConfig.Set("RSBot.Area.Region", pos.Region); + PlayerConfig.Set("RSBot.Area.Region", pos.Region.Id); PlayerConfig.Set("RSBot.Area.X", pos.XOffset); PlayerConfig.Set("RSBot.Area.Y", pos.YOffset); PlayerConfig.Set("RSBot.Area.Z", pos.ZOffset); @@ -804,7 +954,7 @@ private void menuSelectProfile_Click(object sender, EventArgs e) var oldSroPath = GlobalConfig.Get("RSBot.SilkroadDirectory", ""); //We need this to check if the sro directories are different - var tempNewConfig = new Config(ProfileManager.GetProfileFile(dialog.SelectedProfile)); + var tempNewConfig = new RSBot.Core.ConfigContainer(ProfileManager.GetProfileFile(dialog.SelectedProfile)); if (oldSroPath != tempNewConfig.Get("RSBot.SilkroadDirectory", "")) if ( @@ -828,7 +978,7 @@ private void menuSelectProfile_Click(object sender, EventArgs e) return; //Reload player config - PlayerConfig.Load(Game.Player.Name); + RSBot.Core.Config.LoadPlayer(Game.Player.Name); //A little hack to tell all plugins to reload their UI EventManager.FireEvent("OnLoadCharacter"); diff --git a/Application/OasisBot/Views/SplashScreen.cs b/Application/OasisBot/Views/SplashScreen.cs index ddd8e8d2..4cad2851 100644 --- a/Application/OasisBot/Views/SplashScreen.cs +++ b/Application/OasisBot/Views/SplashScreen.cs @@ -147,7 +147,7 @@ private bool LoadProfileConfig() { if (!ProfileManager.IsProfileLoadedByArgs) { - if (ProfileManager.ShowProfileDialog) + if (ProfileManager.Profiles.Length > 1) { var dialog = new ProfileSelectionDialog(); if (dialog.ShowDialog() != DialogResult.Cancel) @@ -162,7 +162,7 @@ private bool LoadProfileConfig() //Configured profile could not be found. Fallback to default profile if (!string.IsNullOrEmpty(selectedProfile) && !File.Exists(profilePath)) - selectedProfile = "Default"; + selectedProfile = "default"; ProfileManager.SetSelectedProfile(selectedProfile); } diff --git a/Botbases/RSBot.Training/Bundle/Loop/LoopBundle.cs b/Botbases/RSBot.Training/Bundle/Loop/LoopBundle.cs index 8a2041e0..eccfe88b 100644 --- a/Botbases/RSBot.Training/Bundle/Loop/LoopBundle.cs +++ b/Botbases/RSBot.Training/Bundle/Loop/LoopBundle.cs @@ -9,6 +9,8 @@ namespace RSBot.Training.Bundle.Loop; internal class LoopBundle : IBundle { + private System.DateTime _lastAutopathAttempt = System.DateTime.MinValue; + /// /// Gets the configuration. /// @@ -78,6 +80,7 @@ public void Stop() ShoppingManager.Stop(); Running = false; + _lastAutopathAttempt = System.DateTime.MinValue; } /// @@ -165,6 +168,14 @@ public void CheckForWalkbackScript(bool startFromTown = false) if (Config.WalkScript == null || !File.Exists(Config.WalkScript)) { + if (Container.Bot.Area.Position.DistanceToPlayer() <= Container.Bot.Area.Radius + 50) + return; + + if (System.DateTime.UtcNow - _lastAutopathAttempt < System.TimeSpan.FromSeconds(30)) + return; + + _lastAutopathAttempt = System.DateTime.UtcNow; + Log.Notify("No walkback script found. Attempting to generate a dynamic path..."); if (NavigationManager.CalculatePathToTrainingArea()) { diff --git a/Botbases/RSBot.Training/Components/TrainingAreaScriptCommand.cs b/Botbases/RSBot.Training/Components/TrainingAreaScriptCommand.cs index d68c7616..5e10156e 100644 --- a/Botbases/RSBot.Training/Components/TrainingAreaScriptCommand.cs +++ b/Botbases/RSBot.Training/Components/TrainingAreaScriptCommand.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using RSBot.Core; using RSBot.Core.Components.Scripting; using RSBot.Core.Event; @@ -59,7 +59,7 @@ public bool Execute(string[] arguments = null) ) return false; - PlayerConfig.Set("RSBot.Area.Region", region); + PlayerConfig.Set("RSBot.Area.Region", region.Id); PlayerConfig.Set("RSBot.Area.X", xPos); PlayerConfig.Set("RSBot.Area.Y", yPos); PlayerConfig.Set("RSBot.Area.Z", zPos); diff --git a/Botbases/RSBot.Training/TrainingBase.cs b/Botbases/RSBot.Training/TrainingBase.cs index 2354670d..b6e1aa5b 100644 --- a/Botbases/RSBot.Training/TrainingBase.cs +++ b/Botbases/RSBot.Training/TrainingBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using RSBot.Core; using RSBot.Core.Components; using RSBot.Core.Objects; @@ -34,7 +34,7 @@ public void Tick() return; //Begin the loopback if needed - if (Container.Bot.Area.Position.DistanceToPlayer() > 80) + if (Container.Bot.Area.Position.DistanceToPlayer() > Container.Bot.Area.Radius + 50) Bundles.Loop.Start(); if (Bundles.Loop.Running) diff --git a/Botbases/RSBot.Training/TrainingManager.cs b/Botbases/RSBot.Training/TrainingManager.cs index cf9bce54..b6a5fdeb 100644 --- a/Botbases/RSBot.Training/TrainingManager.cs +++ b/Botbases/RSBot.Training/TrainingManager.cs @@ -1,4 +1,4 @@ -using System.Timers; +using System.Timers; using RSBot.Core; using RSBot.Core.Components; using RSBot.Core.Event; @@ -22,7 +22,7 @@ public static void ApplyTrainingArea(float x, float y, ushort region) { Position pos = new(x, y, region); - PlayerConfig.Set("RSBot.Area.Region", pos.Region); + PlayerConfig.Set("RSBot.Area.Region", pos.Region.Id); PlayerConfig.Set("RSBot.Area.X", pos.XOffset); PlayerConfig.Set("RSBot.Area.Y", pos.YOffset); PlayerConfig.Set("RSBot.Area.Z", pos.ZOffset); diff --git a/Botbases/RSBot.Training/Views/Dialogs/TrainingAreasDialog.cs b/Botbases/RSBot.Training/Views/Dialogs/TrainingAreasDialog.cs index c3afc9ee..10c756d8 100644 --- a/Botbases/RSBot.Training/Views/Dialogs/TrainingAreasDialog.cs +++ b/Botbases/RSBot.Training/Views/Dialogs/TrainingAreasDialog.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Windows.Forms; using RSBot.Core; @@ -35,7 +35,7 @@ private void buttonAccept_Click(object sender, EventArgs e) return; } - PlayerConfig.Set("RSBot.Area.Region", trainingArea.Position.Region); + PlayerConfig.Set("RSBot.Area.Region", trainingArea.Position.Region.Id); PlayerConfig.Set("RSBot.Area.X", trainingArea.Position.XOffset); PlayerConfig.Set("RSBot.Area.Y", trainingArea.Position.YOffset); PlayerConfig.Set("RSBot.Area.Z", trainingArea.Position.ZOffset); diff --git a/Botbases/RSBot.Training/Views/Main.cs b/Botbases/RSBot.Training/Views/Main.cs index 39ded006..4adf8bc5 100644 --- a/Botbases/RSBot.Training/Views/Main.cs +++ b/Botbases/RSBot.Training/Views/Main.cs @@ -212,7 +212,7 @@ private void btnGetCurrent_Click(object sender, EventArgs e) { var pos = Game.Player.Position; - PlayerConfig.Set("RSBot.Area.Region", pos.Region); + PlayerConfig.Set("RSBot.Area.Region", pos.Region.Id); PlayerConfig.Set("RSBot.Area.X", pos.XOffset); PlayerConfig.Set("RSBot.Area.Y", pos.YOffset); PlayerConfig.Set("RSBot.Area.Z", pos.ZOffset); diff --git a/Library/RSBot.Core/Components/ProfileManager.cs b/Library/RSBot.Core/Components/ProfileManager.cs index c9939997..4a78d91f 100644 --- a/Library/RSBot.Core/Components/ProfileManager.cs +++ b/Library/RSBot.Core/Components/ProfileManager.cs @@ -8,11 +8,6 @@ namespace RSBot.Core.Components; public class ProfileManager { - /// - /// The profile config - /// - private static readonly Config _config; - /// /// Get active profiles /// @@ -23,13 +18,29 @@ public class ProfileManager /// static ProfileManager() { - _config = new Config(GetProfileConfigFileName()); - _profiles = new ObservableCollection(_config.GetArray("RSBot.Profiles", '|')); + Config.Initialize(); + + var loadedProfiles = GeneralConfig.GetArray("RSBot.Profiles"); + string[] reservedNames = { "Settings", "Logs" }; + var validProfiles = loadedProfiles + .Select(p => p.ToLowerInvariant()) + .Where(p => !reservedNames.Any(n => n.Equals(p, StringComparison.OrdinalIgnoreCase))) + .Distinct() + .ToList(); - if (!_profiles.Contains("Default")) - _profiles.Insert(0, "Default"); + _profiles = new ObservableCollection(validProfiles); + + var isNew = _profiles.Count == 0; + if (isNew) + _profiles.Insert(0, "default"); _profiles.CollectionChanged += Profiles_CollectionChanged; + + if (isNew || loadedProfiles.Length != validProfiles.Count) + { + GeneralConfig.SetArray("RSBot.Profiles", _profiles); + GeneralConfig.Save(); + } } /// @@ -48,22 +59,14 @@ static ProfileManager() public static string SelectedCharacter { get; set; } /// - /// The selected profile + /// The selected account /// - public static string SelectedProfile => _config.Get("RSBot.SelectedProfile", "Default"); + public static string SelectedAccount { get; set; } /// - /// Show the profile dialog true; otherwise false + /// The selected profile /// - public static bool ShowProfileDialog - { - get => _config.Get("RSBot.ShowProfileDialog", false); - set - { - _config.Set("RSBot.ShowProfileDialog", value); - _config.Save(); - } - } + public static string SelectedProfile { get; set; } = "default"; /// /// There have any value in the collection true; otherwise false @@ -78,8 +81,8 @@ public static bool Any() /// private static void Profiles_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - _config.SetArray("RSBot.Profiles", _profiles, "|"); - _config.Save(); + GeneralConfig.SetArray("RSBot.Profiles", _profiles); + GeneralConfig.Save(); } /// @@ -88,11 +91,12 @@ private static void Profiles_CollectionChanged(object? sender, NotifyCollectionC /// The profile public static bool SetSelectedProfile(string profile) { - if (!_profiles.Any(p => p == profile)) + var normalized = profile.ToLowerInvariant(); + if (!_profiles.Any(p => p == normalized)) return false; - _config.Set("RSBot.SelectedProfile", profile); - _config.Save(); + SelectedProfile = normalized; + Config.LoadProfile(normalized); return true; } @@ -103,7 +107,7 @@ public static bool SetSelectedProfile(string profile) /// The profile public static bool ProfileExists(string profile) { - return _profiles.Any(p => p.Equals(profile, StringComparison.InvariantCultureIgnoreCase)); + return _profiles.Any(p => p == profile.ToLowerInvariant()); } /// @@ -114,27 +118,28 @@ public static bool ProfileExists(string profile) /// Is created true; otherwise false public static bool Add(string profile, bool useAsBase = false) { - string[] reservedNames = { "Profiles", "Default", "Settings" }; + var normalized = profile.ToLowerInvariant(); + string[] reservedNames = { "settings", "logs" }; - if (reservedNames.Any(n => n.Equals(profile, StringComparison.InvariantCultureIgnoreCase))) + if (reservedNames.Contains(normalized)) return false; - if (ProfileExists(profile)) + if (ProfileExists(normalized)) { - SetSelectedProfile(profile); + SetSelectedProfile(normalized); return true; } - _profiles.Add(profile); + _profiles.Add(normalized); if (useAsBase) - CopyOldProfileData(profile); + MigrationManager.CopyProfileData(SelectedProfile, profile); var newProfileDirectory = GetProfileDirectory(profile); if (!Directory.Exists(newProfileDirectory)) Directory.CreateDirectory(newProfileDirectory); - SetSelectedProfile(profile); + SetSelectedProfile(normalized); return true; } @@ -146,46 +151,12 @@ public static bool Add(string profile, bool useAsBase = false) /// Is removed true; otherwise false public static bool Remove(string profile) { - return _profiles.Remove(profile); - } - - /// - /// Copies the old profile data to the new profile. - /// - /// Name of the profile. - private static void CopyOldProfileData(string profile) - { - try - { - var oldProfileFilePath = GetProfileFile(SelectedProfile); - var newProfileFilePath = GetProfileFile(profile); - var oldAutoLoginFile = Path.Combine(GetProfileDirectory(SelectedProfile), "autologin.data"); - var newAutoLoginFile = Path.Combine(GetProfileDirectory(profile), "autologin.data"); - - if (File.Exists(oldProfileFilePath)) - File.Copy(oldProfileFilePath, newProfileFilePath); - - if (File.Exists(oldAutoLoginFile)) - File.Copy(oldAutoLoginFile, newAutoLoginFile); - } - catch (Exception ex) - { - Log.Warn($"Could not copy old profile data to the new profile: {ex.Message}"); - } - } - - /// - /// Get profile config file name - /// - /// - public static string GetProfileConfigFileName() - { - return Path.Combine(Kernel.BasePath, "User", "Profiles.rs"); + return _profiles.Remove(profile.ToLowerInvariant()); } public static string GetProfileFile(string profileName) { - return Path.Combine(Kernel.BasePath, "User", $"{profileName}.rs"); + return Path.Combine(Kernel.BasePath, "User", $"{profileName}.json"); } public static string GetProfileDirectory(string profileName) diff --git a/Library/RSBot.Core/Config/Config.cs b/Library/RSBot.Core/Config/Config.cs index b741ffe2..1cf37495 100644 --- a/Library/RSBot.Core/Config/Config.cs +++ b/Library/RSBot.Core/Config/Config.cs @@ -1,211 +1,306 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; namespace RSBot.Core; -public class Config +public class ConfigContainer { - /// - /// The object that stores the configuration - /// - private readonly ConcurrentDictionary _config; - - /// - /// Gets the path. - /// private readonly string _path; + private ConcurrentDictionary _data; - /// - /// Loads the specified file. - /// - /// The file. - public Config(string file) + public ConfigContainer(string path) { - _path = file; + _path = path; + Load(); + } - CheckPath(); + public void Load() + { + _data = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _config = new ConcurrentDictionary(); - foreach (var line in File.ReadAllLines(_path)) + if (!File.Exists(_path)) { - if (string.IsNullOrWhiteSpace(line)) - continue; - - var key = line.Split('{')[0]; - var value = line.Split('{')[1].Split('}')[0]; + Save(); + return; + } - if (!_config.ContainsKey(key)) - _config.TryAdd(key, value); + try + { + var json = File.ReadAllText(_path); + if (!string.IsNullOrWhiteSpace(json)) + { + var dict = JsonSerializer.Deserialize>(json); + if (dict != null) + { + foreach (var kvp in dict) + { + _data[kvp.Key] = kvp.Value; + } + } + } + } + catch (Exception ex) + { + Log.Warn($"[ConfigContainer] Failed to load {_path}: {ex.Message}"); } } - /// - /// gets is loaded - /// - private bool _isLoaded => _config != null; + public void Save() + { + try + { + var directory = Path.GetDirectoryName(_path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(_data, options); + File.WriteAllText(_path, json); + } + catch (Exception ex) + { + Log.Warn($"[ConfigContainer] Failed to save {_path}: {ex.Message}"); + } + } - /// - /// Existses the specified key. - /// - /// The key. - /// public bool Exists(string key) { - if (!_isLoaded) - return false; - - return _config.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value); + return _data.ContainsKey(key); } - /// - /// Gets the specified key. - /// - /// The key. - /// The default value. public T Get(string key, T defaultValue = default) { - if (!_isLoaded) - return (T)Convert.ChangeType(false, typeof(T)); - - if (!_config.ContainsKey(key)) + if (!_data.TryGetValue(key, out var element)) { Set(key, defaultValue); - return defaultValue; } - var value = _config[key]; - if (string.IsNullOrEmpty(value)) + try + { + // If T is not string, but element is string, try to parse it for legacy compatibility + if (typeof(T) != typeof(string) && element.ValueKind == JsonValueKind.String) + { + var str = element.GetString(); + if (typeof(T) == typeof(bool)) + { + if (bool.TryParse(str, out var b)) + return (T)(object)b; + } + else if (typeof(T) == typeof(int)) + { + if (int.TryParse(str, out var i)) + return (T)(object)i; + } + else if (typeof(T) == typeof(uint)) + { + if (uint.TryParse(str, out var ui)) + return (T)(object)ui; + } + else if (typeof(T) == typeof(double)) + { + if ( + double.TryParse( + str, + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, + out var d + ) + ) + return (T)(object)d; + } + else if (typeof(T) == typeof(float)) + { + if ( + float.TryParse( + str, + System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, + out var f + ) + ) + return (T)(object)f; + } + + try + { + return (T)Convert.ChangeType(str, typeof(T)); + } + catch { } + } + + return JsonSerializer.Deserialize(element.GetRawText()); + } + catch + { return defaultValue; - - return (T)Convert.ChangeType(value, typeof(T)); + } } - /// - /// Gets the specified key. - /// - /// The key. - /// The default value. - public TEnum GetEnum(string key, TEnum defaultValue) + public TEnum GetEnum(string key, TEnum defaultValue = default) where TEnum : struct { - if (!_isLoaded) - return default; - - if (!_config.TryGetValue(key, out var value)) + if (!_data.TryGetValue(key, out var element)) { Set(key, defaultValue); - value = defaultValue.ToString(); + return defaultValue; } - TEnum result; - if (!Enum.TryParse(value, out result)) - return default; - - return result; + try + { + var rawText = element.GetRawText(); + if (Enum.TryParse(rawText.Trim('"'), out var parsedEnum)) + { + return parsedEnum; + } + return defaultValue; + } + catch + { + return defaultValue; + } } - /// - /// Sets the specified key inside the config. - /// - /// The key. - /// The value. public void Set(string key, T value) { - var setValue = value == null ? string.Empty : value.ToString(); - _config.AddOrUpdate(key, setValue, (k, v) => setValue); + var json = JsonSerializer.SerializeToElement(value); + _data[key] = json; } - /// - /// Check directories - /// - private void CheckPath() + public T[] GetArray( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) { - var directory = Path.GetDirectoryName(_path); - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory); + if (!_data.TryGetValue(key, out var element)) + return Array.Empty(); - if (!File.Exists(_path)) - File.Create(_path).Dispose(); + if (element.ValueKind == JsonValueKind.Array) + { + try + { + return JsonSerializer.Deserialize(element.GetRawText()) ?? Array.Empty(); + } + catch + { + return Array.Empty(); + } + } + else if (element.ValueKind == JsonValueKind.String) + { + var str = element.GetString(); + if (string.IsNullOrEmpty(str)) + return Array.Empty(); + + var parts = str.Split(new[] { delimiter }, options); + var result = new T[parts.Length]; + for (int i = 0; i < parts.Length; i++) + { + try + { + result[i] = (T)Convert.ChangeType(parts[i], typeof(T)); + } + catch + { + result[i] = default; + } + } + return result; + } + + return Array.Empty(); } - /// - /// Saves the specified file. - /// - /// The file. - public void Save() + public TEnum[] GetEnums( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) + where TEnum : struct { - if (!_isLoaded || string.IsNullOrWhiteSpace(_path)) - return; - - CheckPath(); + if (!_data.TryGetValue(key, out var element)) + return Array.Empty(); - var serializedConfig = new string[_config.Count]; - var index = 0; - - foreach (var element in _config.OrderBy(c => c.Key)) + if (element.ValueKind == JsonValueKind.Array) { - serializedConfig[index] = element.Key + "{" + element.Value + "}"; - index++; + try + { + return JsonSerializer.Deserialize(element.GetRawText()) ?? Array.Empty(); + } + catch + { + return Array.Empty(); + } + } + else if (element.ValueKind == JsonValueKind.String) + { + var str = element.GetString(); + if (string.IsNullOrEmpty(str)) + return Array.Empty(); + + var parts = str.Split(new[] { delimiter }, options); + var result = new List(); + foreach (var part in parts) + { + if (Enum.TryParse(part, out var val)) + { + result.Add(val); + } + } + return result.ToArray(); } - File.WriteAllLines(_path, serializedConfig); + return Array.Empty(); } - /// - /// Sets the array. - /// - /// The key. - /// The values. - /// The delimiter. public void SetArray(string key, IEnumerable values, string delimiter = ",") { - if (values == null) - return; - - Set(key, string.Join(delimiter, values)); + Set(key, values); } - /// - /// Gets the array. - /// - /// The key. - /// The delimiter. - /// - public T[] GetArray( - string key, - char delimiter = ',', - StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries - ) + public void Remove(string key) { - if (!_isLoaded) - return new T[] { }; + _data.TryRemove(key, out _); + } +} + +public static class Config +{ + public static ConfigContainer Settings { get; private set; } + public static ConfigContainer Profile { get; private set; } + public static ConfigContainer Player { get; private set; } + public static bool MigrationTriggered { get; private set; } - var data = Get(key)?.Split(new[] { delimiter }, options); - if (data == null || data.Length == 0) - return new T[] { }; + public static void Initialize() + { + var settingsPath = Path.Combine(Kernel.BasePath, "User", "settings.json"); + Settings = new ConfigContainer(settingsPath); - return data?.Select(p => (T)Convert.ChangeType(p, typeof(T))).ToArray(); + // Run migrations + MigrationTriggered = MigrationManager.MigrateLegacyConfigs(); } - /// - /// Get array the specified key. - /// - /// The key. - /// The default value. - public TEnum[] GetEnums(string key, char delimiter = ',') - where TEnum : struct + public static void LoadProfile(string profileName) { - if (!_isLoaded) - return new TEnum[] { }; - - var data = Get(key)?.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries); - if (data == null || data.Length == 0) - return new TEnum[] { }; + var profilePath = Path.Combine(Kernel.BasePath, "User", $"{profileName}.json"); + Profile = new ConfigContainer(profilePath); + } - return data?.Select(p => Enum.Parse(p)).ToArray(); + public static void LoadPlayer(string characterName) + { + var playerPath = Path.Combine( + Kernel.BasePath, + "User", + Components.ProfileManager.SelectedProfile, + $"{characterName}.json" + ); + Player = new ConfigContainer(playerPath); } } diff --git a/Library/RSBot.Core/Config/GeneralConfig.cs b/Library/RSBot.Core/Config/GeneralConfig.cs index 3b8726aa..d6fa425e 100644 --- a/Library/RSBot.Core/Config/GeneralConfig.cs +++ b/Library/RSBot.Core/Config/GeneralConfig.cs @@ -1,75 +1,70 @@ using System; -using System.IO; +using System.Collections.Generic; namespace RSBot.Core; public static class GeneralConfig { /// - /// The config + /// Checks if the specified key exists in the settings config. /// - private static Config _config; + public static bool Exists(string key) => Config.Settings != null && Config.Settings.Exists(key); /// - /// Load config from file + /// Gets a value from the settings config. /// - public static void Load() - { - var path = Path.Combine(Kernel.BasePath, "User", "Settings.rs"); - _config = new Config(path); - } + public static T Get(string key, T defaultValue = default) => + Config.Settings != null ? Config.Settings.Get(key, defaultValue) : defaultValue; /// - /// Returns a value indicating if the given config key exists. + /// Gets an enum value from the settings config. /// - /// The key. - /// - public static bool Exists(string key) - { - if (_config == null) - Load(); - return _config.Exists(key); - } + public static TEnum GetEnum(string key, TEnum defaultValue = default) + where TEnum : struct => Config.Settings != null ? Config.Settings.GetEnum(key, defaultValue) : defaultValue; /// - /// Gets the specified key. + /// Sets a value in the settings config. /// - /// The key. - /// The default value. - public static T Get(string key, T defaultValue = default) - { - if (_config == null) - Load(); - return _config.Get(key, defaultValue); - } + public static void Set(string key, T value) => Config.Settings?.Set(key, value); /// - /// Sets the specified key inside the config. + /// Gets an array from the settings config. /// - /// The key. - /// The value. - public static void Set(string key, T value) - { - if (_config == null) - Load(); - _config.Set(key, value); - } + public static T[] GetArray( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) => Config.Settings != null ? Config.Settings.GetArray(key, delimiter, options) : Array.Empty(); /// - /// Saves the specified file. + /// Gets enums array from the settings config. /// - public static void Save() - { - if (_config == null) - return; + public static TEnum[] GetEnums( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) + where TEnum : struct => + Config.Settings != null ? Config.Settings.GetEnums(key, delimiter, options) : Array.Empty(); - try - { - _config.Save(); - } - catch (Exception ex) - { - Log.Debug($"[GeneralConfig] Could not save settings: {ex.Message}"); - } - } + /// + /// Sets an array in the settings config. + /// + public static void SetArray(string key, IEnumerable values, string delimiter = ",") => + Config.Settings?.SetArray(key, values, delimiter); + + /// + /// Removes a key from the settings config. + /// + public static void Remove(string key) => Config.Settings?.Remove(key); + + /// + /// Reloads the settings config. + /// + public static void Load() => Config.Settings?.Load(); + + /// + /// Saves the settings config. + /// + public static void Save() => Config.Settings?.Save(); } diff --git a/Library/RSBot.Core/Config/GlobalConfig.cs b/Library/RSBot.Core/Config/GlobalConfig.cs index 1613ab16..ebe7742f 100644 --- a/Library/RSBot.Core/Config/GlobalConfig.cs +++ b/Library/RSBot.Core/Config/GlobalConfig.cs @@ -1,130 +1,70 @@ -using System; +using System; using System.Collections.Generic; -using System.IO; -using RSBot.Core.Components; -using RSBot.Core.Event; namespace RSBot.Core; public static class GlobalConfig { /// - /// The config + /// Checks if the specified key exists in the profile config. /// - private static Config _config; + public static bool Exists(string key) => Config.Profile != null && Config.Profile.Exists(key); /// - /// Load config from file + /// Gets a value from the profile config. /// - public static void Load() - { - var path = Path.Combine(Kernel.BasePath, "User", ProfileManager.SelectedProfile + ".rs"); - - _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") - { - _config.Set("RSBot.BotName", "RSBot.Training"); - _config.Save(); - } - - Log.Notify("[Global] settings have been loaded!"); - } + public static T Get(string key, T defaultValue = default) => + Config.Profile != null ? Config.Profile.Get(key, defaultValue) : defaultValue; /// - /// Returns a value indicating if the given config key exists. + /// Gets an enum value from the profile config. /// - /// The key. - /// - public static bool Exists(string key) - { - if (_config == null) - return false; - - return _config.Exists(key); - } - - /// - /// Gets the specified key. - /// - /// The key. - /// The default value. - public static T Get(string key, T defaultValue = default) - { - if (_config == null) - return defaultValue; - - return _config.Get(key, defaultValue); - } + public static TEnum GetEnum(string key, TEnum defaultValue = default) + where TEnum : struct => Config.Profile != null ? Config.Profile.GetEnum(key, defaultValue) : defaultValue; /// - /// Gets the enum value with specified key. + /// Sets a value in the profile config. /// - /// The key. - /// The default value. - public static TEnum GetEnum(string key, TEnum defaultValue = default) - where TEnum : struct - { - if (_config == null) - return defaultValue; - - return _config.GetEnum(key, defaultValue); - } + public static void Set(string key, T value) => Config.Profile?.Set(key, value); /// - /// Sets the specified key inside the config. + /// Gets an array from the profile config. /// - /// The key. - /// The value. - public static void Set(string key, T value) - { - if (_config != null) - _config.Set(key, value); - } + public static T[] GetArray( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) => Config.Profile != null ? Config.Profile.GetArray(key, delimiter, options) : Array.Empty(); /// - /// Gets the array. + /// Gets enums array from the profile config. /// - /// The key. - /// The delimiter. - /// - public static T[] GetArray( + public static TEnum[] GetEnums( string key, char delimiter = ',', StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries ) - { - if (_config == null) - return new T[] { }; - - return _config.GetArray(key, delimiter, options); - } + where TEnum : struct => + Config.Profile != null ? Config.Profile.GetEnums(key, delimiter, options) : Array.Empty(); /// - /// Sets the array. + /// Sets an array in the profile config. /// - /// The key. - /// The values. - /// The delimiter. - public static void SetArray(string key, IEnumerable values, string delimiter = ",") - { - if (_config != null) - _config.SetArray(key, values, delimiter); - } + public static void SetArray(string key, IEnumerable values, string delimiter = ",") => + Config.Profile?.SetArray(key, values, delimiter); /// - /// Saves the specified file. + /// Removes a key from the profile config. /// - /// The file. - public static void Save() - { - if (_config == null) - return; + public static void Remove(string key) => Config.Profile?.Remove(key); - _config.Save(); + /// + /// Reloads the profile config. + /// + public static void Load() => Config.Profile?.Load(); - Log.Notify("[Global] settings have been saved!"); - EventManager.FireEvent("OnSaveGlobalConfig"); - } + /// + /// Saves the profile config. + /// + public static void Save() => Config.Profile?.Save(); } diff --git a/Library/RSBot.Core/Config/MigrationManager.cs b/Library/RSBot.Core/Config/MigrationManager.cs new file mode 100644 index 00000000..fdc54ce2 --- /dev/null +++ b/Library/RSBot.Core/Config/MigrationManager.cs @@ -0,0 +1,615 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using RSBot.Core.Components; + +namespace RSBot.Core; + +public static class MigrationManager +{ + private static Dictionary ParseLegacyRsFile(string path) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!File.Exists(path)) + return dict; + + foreach (var line in File.ReadAllLines(path)) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + var splitIndex = line.IndexOf('{'); + if (splitIndex == -1) + continue; + + var key = line.Substring(0, splitIndex); + var rest = line.Substring(splitIndex + 1); + var closingIndex = rest.LastIndexOf('}'); + if (closingIndex == -1) + continue; + + var value = rest.Substring(0, closingIndex); + + if (!dict.ContainsKey(key)) + dict[key] = value; + } + + return dict; + } + + private static bool IsMigrationNeeded() + { + var userDirectory = Path.Combine(Kernel.BasePath, "User"); + if (!Directory.Exists(userDirectory)) + return false; + + // 1. Check legacy Settings/Profiles + if ( + File.Exists(Path.Combine(userDirectory, "Settings.rs")) + || File.Exists(Path.Combine(userDirectory, "Profiles.rs")) + ) + return true; + + // 2. Check profile *.rs files directly under User/ + var files = Directory.GetFiles(userDirectory, "*.rs"); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + if ( + !fileName.Equals("Settings", StringComparison.OrdinalIgnoreCase) + && !fileName.Equals("Profiles", StringComparison.OrdinalIgnoreCase) + ) + { + return true; + } + } + + var directories = Directory.GetDirectories(userDirectory); + + // 3. Check autologin.data in profile directories + foreach (var dir in directories) + { + var profileName = Path.GetFileName(dir); + if (profileName.Equals("Logs", StringComparison.OrdinalIgnoreCase)) + continue; + + if (File.Exists(Path.Combine(dir, "autologin.data"))) + return true; + + // 4. Check Character *.rs files inside profile subdirectories + if (Directory.GetFiles(dir, "*.rs").Length > 0) + return true; + } + + // 5. Check NormalizeProfileCasing json files + var jsonFiles = Directory + .GetFiles(userDirectory, "*.json") + .Where(f => !Path.GetFileNameWithoutExtension(f).Equals("settings", StringComparison.OrdinalIgnoreCase)) + .ToList(); + foreach (var file in jsonFiles) + { + var oldName = Path.GetFileNameWithoutExtension(file); + var newName = oldName.ToLowerInvariant(); + if (oldName != newName) + return true; + } + + // 6. Check NormalizeProfileCasing directories + var profileDirs = Directory + .GetDirectories(userDirectory) + .Where(d => !Path.GetFileName(d).Equals("Logs", StringComparison.OrdinalIgnoreCase)) + .ToList(); + foreach (var dir in profileDirs) + { + var oldName = Path.GetFileName(dir); + var newName = oldName.ToLowerInvariant(); + if (oldName != newName) + return true; + } + + return false; + } + + private static void BackupUserDirectory() + { + try + { + var userDir = Path.Combine(Kernel.BasePath, "User"); + var backupDir = Path.Combine(Kernel.BasePath, "User_backup"); + + if (Directory.Exists(backupDir)) + { + Directory.Delete(backupDir, true); + } + + CopyDirectory(userDir, backupDir); + Log.Notify("[MigrationManager] Created backup of User/ to User_backup/."); + } + catch (Exception ex) + { + Log.Warn($"[MigrationManager] Failed to create User/ backup: {ex.Message}"); + } + } + + private static void CopyDirectory(string sourceDir, string destinationDir) + { + Directory.CreateDirectory(destinationDir); + + foreach (var file in Directory.GetFiles(sourceDir)) + { + var destFile = Path.Combine(destinationDir, Path.GetFileName(file)); + File.Copy(file, destFile, true); + } + + foreach (var subDir in Directory.GetDirectories(sourceDir)) + { + var destSubDir = Path.Combine(destinationDir, Path.GetFileName(subDir)); + CopyDirectory(subDir, destSubDir); + } + } + + /// + /// Migrates all legacy .rs and separate autologin files to unified JSON structure. + /// + public static bool MigrateLegacyConfigs() + { + var migrationPerformed = false; + var userDirectory = Path.Combine(Kernel.BasePath, "User"); + if (!Directory.Exists(userDirectory)) + return false; + + if (IsMigrationNeeded()) + { + BackupUserDirectory(); + } + + try + { + // 1. Migrate settings.rs to settings.json + var legacySettingsPath = Path.Combine(userDirectory, "Settings.rs"); + var newSettingsPath = Path.Combine(userDirectory, "settings.json"); + + // Also check for legacy Profiles.rs that we used previously + var legacyProfilesPath = Path.Combine(userDirectory, "Profiles.rs"); + if (File.Exists(legacyProfilesPath)) + { + migrationPerformed = true; + var legacyProfiles = ParseLegacyRsFile(legacyProfilesPath); + if (legacyProfiles.TryGetValue("RSBot.Profiles", out var profiles)) + { + GeneralConfig.Set( + "RSBot.Profiles", + profiles.Split('|').Select(p => p.ToLowerInvariant()).ToArray() + ); + } + if (legacyProfiles.TryGetValue("RSBot.SelectedProfile", out var selectedProfile)) + { + ProfileManager.SelectedProfile = selectedProfile.ToLowerInvariant(); + } + try + { + File.Delete(legacyProfilesPath); + } + catch { } + } + + if (File.Exists(legacySettingsPath)) + { + migrationPerformed = true; + var legacySettings = ParseLegacyRsFile(legacySettingsPath); + foreach (var kvp in legacySettings) + { + // Filter out legacy unused keys + if ( + kvp.Key.Equals("RSBot.SelectedProfile", StringComparison.OrdinalIgnoreCase) + || kvp.Key.Equals("RSBot.ShowProfileDialog", StringComparison.OrdinalIgnoreCase) + ) + { + continue; + } + + // For profiles, store it as array if it contains delimiters + if (kvp.Key.Equals("RSBot.Profiles", StringComparison.OrdinalIgnoreCase)) + { + GeneralConfig.Set(kvp.Key, kvp.Value.Split('|').Select(p => p.ToLowerInvariant()).ToArray()); + } + else + { + GeneralConfig.Set(kvp.Key, kvp.Value); + } + } + GeneralConfig.Save(); + try + { + File.Delete(legacySettingsPath); + } + catch { } + Log.Notify("[MigrationManager] Migrated Settings.rs to settings.json."); + } + + // 2. Migrate Profile files (*.rs directly under User/) + var files = Directory.GetFiles(userDirectory, "*.rs"); + foreach (var file in files) + { + var fileName = Path.GetFileNameWithoutExtension(file); + if ( + fileName.Equals("Settings", StringComparison.OrdinalIgnoreCase) + || fileName.Equals("Profiles", StringComparison.OrdinalIgnoreCase) + ) + { + try + { + File.Delete(file); + } + catch { } + continue; + } + + migrationPerformed = true; + + // Parse legacy profile config + var legacyProfileData = ParseLegacyRsFile(file); + var newProfilePath = Path.Combine(userDirectory, $"{fileName}.json"); + var profileConfig = new ConfigContainer(newProfilePath); + + foreach (var kvp in legacyProfileData) + { + // PR #934 Migration: "RSBot.Default" was moved to "RSBot.Training" + if ( + kvp.Key.Equals("RSBot.BotName", StringComparison.OrdinalIgnoreCase) + && kvp.Value == "RSBot.Default" + ) + { + profileConfig.Set(kvp.Key, "RSBot.Training"); + } + else + { + profileConfig.Set(kvp.Key, kvp.Value); + } + } + + // Merge Autologin data into profile's config + var profileDirectory = Path.Combine(userDirectory, fileName); + var legacyAutoLoginDataPath = Path.Combine(profileDirectory, "autologin.data"); + + string autoLoginJsonContent = null; + + if (File.Exists(legacyAutoLoginDataPath)) + { + try + { + var buffer = File.ReadAllBytes(legacyAutoLoginDataPath); + if (buffer.Length > 0) + { + if (buffer[0] == '[' || buffer[0] == '{') + { + autoLoginJsonContent = System.Text.Encoding.UTF8.GetString(buffer); + } + else + { + var blowfish = new RSBot.Core.Network.Protocol.Blowfish(); + var decoded = blowfish.Decode(buffer); + autoLoginJsonContent = System.Text.Encoding.UTF8.GetString(decoded).Trim('\0'); + } + } + File.Delete(legacyAutoLoginDataPath); + } + catch (Exception ex) + { + Log.Warn( + $"[MigrationManager] Failed to read legacy autologin.data for profile {fileName}: {ex.Message}" + ); + } + } + + if (!string.IsNullOrEmpty(autoLoginJsonContent)) + { + try + { + var accountsElement = JsonSerializer.Deserialize(autoLoginJsonContent); + profileConfig.Set("Accounts", accountsElement); + Log.Notify($"[MigrationManager] Merged accounts into {fileName}.json config."); + } + catch (Exception ex) + { + Log.Warn( + $"[MigrationManager] Failed to parse and merge autologin accounts JSON for profile {fileName}: {ex.Message}" + ); + } + } + + profileConfig.Save(); + try + { + File.Delete(file); + } + catch { } + Log.Notify($"[MigrationManager] Migrated profile {fileName}.rs to {fileName}.json."); + } + + // 3. Migrate Autologin data (.data) for all existing profile folders + var directories = Directory.GetDirectories(userDirectory); + foreach (var dir in directories) + { + var profileName = Path.GetFileName(dir); + if (profileName.Equals("Logs", StringComparison.OrdinalIgnoreCase)) + continue; + + var legacyAutoLoginDataPath = Path.Combine(dir, "autologin.data"); + + if (File.Exists(legacyAutoLoginDataPath)) + { + var profileJsonPath = Path.Combine(userDirectory, $"{profileName}.json"); + if (File.Exists(profileJsonPath)) + { + var profileConfig = new ConfigContainer(profileJsonPath); + if (!profileConfig.Exists("Accounts")) + { + string autoLoginJsonContent = null; + try + { + var buffer = File.ReadAllBytes(legacyAutoLoginDataPath); + if (buffer.Length > 0) + { + if (buffer[0] == '[' || buffer[0] == '{') + { + autoLoginJsonContent = System.Text.Encoding.UTF8.GetString(buffer); + } + else + { + var blowfish = new RSBot.Core.Network.Protocol.Blowfish(); + var decoded = blowfish.Decode(buffer); + autoLoginJsonContent = System.Text.Encoding.UTF8.GetString(decoded).Trim('\0'); + } + } + } + catch (Exception ex) + { + Log.Warn( + $"[MigrationManager] Failed to read legacy autologin.data for profile {profileName}: {ex.Message}" + ); + } + + if (!string.IsNullOrEmpty(autoLoginJsonContent)) + { + try + { + var accountsElement = JsonSerializer.Deserialize(autoLoginJsonContent); + profileConfig.Set("Accounts", accountsElement); + profileConfig.Save(); + migrationPerformed = true; + Log.Notify($"[MigrationManager] Merged accounts into {profileName}.json config."); + } + catch (Exception ex) + { + Log.Warn( + $"[MigrationManager] Failed to parse and merge autologin accounts JSON for profile {profileName}: {ex.Message}" + ); + } + } + } + + try + { + File.Delete(legacyAutoLoginDataPath); + } + catch { } + } + } + } + + // 4. Migrate Character files (*.rs inside profile subdirectories) + foreach (var dir in directories) + { + var dirName = Path.GetFileName(dir); + if (dirName.Equals("Logs", StringComparison.OrdinalIgnoreCase)) + continue; + + var charFiles = Directory.GetFiles(dir, "*.rs"); + foreach (var charFile in charFiles) + { + migrationPerformed = true; + var charName = Path.GetFileNameWithoutExtension(charFile); + var legacyCharData = ParseLegacyRsFile(charFile); + var newCharPath = Path.Combine(dir, $"{charName}.json"); + var charConfig = new ConfigContainer(newCharPath); + + foreach (var kvp in legacyCharData) + { + charConfig.Set(kvp.Key, kvp.Value); + } + + charConfig.Save(); + try + { + File.Delete(charFile); + } + catch { } + Log.Notify( + $"[MigrationManager] Migrated character config {charName}.rs to {charName}.json in profile {dirName}." + ); + } + } + } + catch (Exception ex) + { + Log.Warn($"[MigrationManager] Error occurred during legacy configurations migration: {ex.Message}"); + } + + if (NormalizeProfileCasing()) + migrationPerformed = true; + + return migrationPerformed; + } + + /// + /// Renames profile directories and JSON files to lowercase. + /// Resolves clashes by appending a numeric suffix. + /// + private static bool NormalizeProfileCasing() + { + var migrated = false; + var userDirectory = Path.Combine(Kernel.BasePath, "User"); + if (!Directory.Exists(userDirectory)) + return false; + + string[] reserved = { "settings", "logs" }; + var takenNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Collect already-lowercase names to detect clashes + foreach (var file in Directory.GetFiles(userDirectory, "*.json")) + { + var name = Path.GetFileNameWithoutExtension(file).ToLowerInvariant(); + takenNames.Add(name); + } + + try + { + // 1. Normalize profile JSON files + var jsonFiles = Directory + .GetFiles(userDirectory, "*.json") + .Where(f => !Path.GetFileNameWithoutExtension(f).Equals("settings", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var renamedProfiles = new Dictionary(); // old name -> new name + + foreach (var file in jsonFiles) + { + var oldName = Path.GetFileNameWithoutExtension(file); + var newName = oldName.ToLowerInvariant(); + + if (oldName == newName) + continue; + + // Resolve clash + var candidate = newName; + var counter = 1; + while (takenNames.Contains(candidate) && !candidate.Equals(oldName, StringComparison.OrdinalIgnoreCase)) + { + candidate = $"{newName}{counter}"; + counter++; + } + + if (reserved.Contains(candidate)) + continue; + + takenNames.Remove(oldName); + takenNames.Add(candidate); + renamedProfiles[oldName] = candidate; + + var dest = Path.Combine(userDirectory, $"{candidate}.json"); + File.Move(file, dest, true); + migrated = true; + Log.Notify($"[MigrationManager] Renamed profile {oldName}.json -> {candidate}.json"); + } + + // 2. Normalize profile directories + var directories = Directory + .GetDirectories(userDirectory) + .Where(d => + { + var name = Path.GetFileName(d); + return !name.Equals("Logs", StringComparison.OrdinalIgnoreCase); + }) + .ToList(); + + foreach (var dir in directories) + { + var oldName = Path.GetFileName(dir); + // Use the same resolved name if we already renamed the json + var newName = renamedProfiles.TryGetValue(oldName, out var resolved) + ? resolved + : oldName.ToLowerInvariant(); + + if (oldName == newName) + continue; + + var dest = Path.Combine(userDirectory, newName); + if (Directory.Exists(dest) && !dest.Equals(dir, StringComparison.OrdinalIgnoreCase)) + { + // Merge: move files into existing directory + foreach (var file in Directory.GetFiles(dir)) + { + var destFile = Path.Combine(dest, Path.GetFileName(file)); + File.Move(file, destFile, true); + } + Directory.Delete(dir, true); + } + else + { + Directory.Move(dir, dest); + } + + migrated = true; + Log.Notify($"[MigrationManager] Renamed profile directory {oldName} -> {newName}"); + } + + // 3. Update profiles array in settings + if (migrated) + { + var profiles = GeneralConfig.GetArray("RSBot.Profiles"); + var normalized = profiles + .Select(p => + { + if (renamedProfiles.TryGetValue(p, out var n)) + return n; + return p.ToLowerInvariant(); + }) + .Distinct() + .ToArray(); + + GeneralConfig.SetArray("RSBot.Profiles", normalized); + GeneralConfig.Save(); + } + } + catch (Exception ex) + { + Log.Warn($"[MigrationManager] Error normalizing profile casing: {ex.Message}"); + } + + return migrated; + } + + /// + /// Copies the old profile data to the new profile. + /// + public static void CopyProfileData(string sourceProfile, string targetProfile) + { + try + { + var oldProfileFilePath = ProfileManager.GetProfileFile(sourceProfile); + var newProfileFilePath = ProfileManager.GetProfileFile(targetProfile); + var oldProfileDir = ProfileManager.GetProfileDirectory(sourceProfile); + var newProfileDir = ProfileManager.GetProfileDirectory(targetProfile); + + if (File.Exists(oldProfileFilePath)) + { + File.Copy(oldProfileFilePath, newProfileFilePath, true); + } + + if (Directory.Exists(oldProfileDir)) + { + if (!Directory.Exists(newProfileDir)) + { + Directory.CreateDirectory(newProfileDir); + } + + // Copy all character JSON files + var files = Directory.GetFiles(oldProfileDir, "*.json"); + foreach (var file in files) + { + var destFile = Path.Combine(newProfileDir, Path.GetFileName(file)); + File.Copy(file, destFile, true); + } + } + } + catch (Exception ex) + { + Log.Warn( + $"[MigrationManager] Could not copy profile data from {sourceProfile} to {targetProfile}: {ex.Message}" + ); + } + } +} diff --git a/Library/RSBot.Core/Config/PlayerConfig.cs b/Library/RSBot.Core/Config/PlayerConfig.cs index 3df4fce0..ec08d3f0 100644 --- a/Library/RSBot.Core/Config/PlayerConfig.cs +++ b/Library/RSBot.Core/Config/PlayerConfig.cs @@ -1,136 +1,70 @@ -using System.Collections.Generic; -using System.IO; -using RSBot.Core.Components; -using RSBot.Core.Event; +using System; +using System.Collections.Generic; namespace RSBot.Core; public static class PlayerConfig { /// - /// The config + /// Checks if the specified key exists in the player config. /// - private static Config _config; + public static bool Exists(string key) => Config.Player != null && Config.Player.Exists(key); /// - /// The config directory + /// Gets a value from the player config. /// - private static string _configDirectory => Path.Combine(Kernel.BasePath, "User", ProfileManager.SelectedProfile); + public static T Get(string key, T defaultValue = default) => + Config.Player != null ? Config.Player.Get(key, defaultValue) : defaultValue; /// - /// Load config from file + /// Gets an enum value from the player config. /// - /// The config file path - public static void Load(string charName) - { - _config = new Config(Path.Combine(_configDirectory, charName + ".rs")); - - Log.Notify("[Player] settings have been loaded!"); - } + public static TEnum GetEnum(string key, TEnum defaultValue = default) + where TEnum : struct => Config.Player != null ? Config.Player.GetEnum(key, defaultValue) : defaultValue; /// - /// Existses the specified key. + /// Sets a value in the player config. /// - /// The key. - /// - public static bool Exists(string key) - { - if (_config == null) - return false; - - return _config.Exists(key); - } + public static void Set(string key, T value) => Config.Player?.Set(key, value); /// - /// Gets the specified key. + /// Gets an array from the player config. /// - /// The key. - /// The default value. - public static T Get(string key, T defaultValue = default) - { - if (_config == null) - return defaultValue; - - return _config.Get(key, defaultValue); - } + public static T[] GetArray( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) => Config.Player != null ? Config.Player.GetArray(key, delimiter, options) : Array.Empty(); /// - /// Gets the enum value with specified key. + /// Gets enums array from the player config. /// - /// The key. - /// The default value. - public static TEnum GetEnum(string key, TEnum defaultValue = default) - where TEnum : struct - { - if (_config == null) - return defaultValue; - - return _config.GetEnum(key, defaultValue); - } + public static TEnum[] GetEnums( + string key, + char delimiter = ',', + StringSplitOptions options = StringSplitOptions.RemoveEmptyEntries + ) + where TEnum : struct => + Config.Player != null ? Config.Player.GetEnums(key, delimiter, options) : Array.Empty(); /// - /// Sets the specified key inside the config. + /// Sets an array in the player config. /// - /// The key. - /// The value. - public static void Set(string key, T value) - { - if (_config != null) - _config.Set(key, value); - } + public static void SetArray(string key, IEnumerable values, string delimiter = ",") => + Config.Player?.SetArray(key, values, delimiter); /// - /// Gets the array. + /// Removes a key from the player config. /// - /// The key. - /// The delimiter. - /// - public static T[] GetArray(string key, char delimiter = ',') - { - if (_config == null) - return new T[] { }; - - return _config.GetArray(key, delimiter); - } + public static void Remove(string key) => Config.Player?.Remove(key); /// - /// Gets the enum value with specified key. + /// Reloads the player config. /// - /// The key. - /// The default value. - public static TEnum[] GetEnums(string key, char delimiter = ',') - where TEnum : struct - { - if (_config == null) - return new TEnum[] { }; - - return _config.GetEnums(key, delimiter); - } - - /// - /// Sets the array. - /// - /// The key. - /// The values. - /// The delimiter. - public static void SetArray(string key, IEnumerable values, string delimiter = ",") - { - if (_config != null) - _config.SetArray(key, values, delimiter); - } + public static void Load() => Config.Player?.Load(); /// - /// Saves the specified file. + /// Saves the player config. /// - /// The file. - public static void Save() - { - if (_config == null) - return; - - _config.Save(); - - Log.Notify("[Player] have been saved!"); - EventManager.FireEvent("OnSavePlayerConfig"); - } + public static void Save() => Config.Player?.Save(); } diff --git a/Library/RSBot.Core/Network/Handler/Agent/CharacterSelection/CharacterSelectionJoinRequest.cs b/Library/RSBot.Core/Network/Handler/Agent/CharacterSelection/CharacterSelectionJoinRequest.cs index dd857acf..d5eec540 100644 --- a/Library/RSBot.Core/Network/Handler/Agent/CharacterSelection/CharacterSelectionJoinRequest.cs +++ b/Library/RSBot.Core/Network/Handler/Agent/CharacterSelection/CharacterSelectionJoinRequest.cs @@ -28,7 +28,7 @@ public void Invoke(Packet packet) { var characterName = packet.ReadString(); - PlayerConfig.Load(characterName); + RSBot.Core.Config.LoadPlayer(characterName); EventManager.FireEvent("OnEnterGame"); } diff --git a/Plugins/RSBot.CommandCenter/Components/Command/AreaCommandExecutor.cs b/Plugins/RSBot.CommandCenter/Components/Command/AreaCommandExecutor.cs index 56c17447..e6af1c8a 100644 --- a/Plugins/RSBot.CommandCenter/Components/Command/AreaCommandExecutor.cs +++ b/Plugins/RSBot.CommandCenter/Components/Command/AreaCommandExecutor.cs @@ -1,4 +1,4 @@ -using RSBot.Core; +using RSBot.Core; using RSBot.Core.Components.Command; using RSBot.Core.Event; @@ -17,7 +17,7 @@ public bool Execute(bool silent) $"[RSBot] Setting training area to X={Game.Player.Position.X:0.00} Y={Game.Player.Position.Y:0.00} R=50" ); - PlayerConfig.Set("RSBot.Area.Region", Game.Player.Position.Region); + PlayerConfig.Set("RSBot.Area.Region", Game.Player.Position.Region.Id); PlayerConfig.Set("RSBot.Area.X", Game.Player.Position.XOffset.ToString("0.0")); PlayerConfig.Set("RSBot.Area.Y", Game.Player.Position.YOffset.ToString("0.0")); PlayerConfig.Set("RSBot.Area.Z", Game.Player.Position.ZOffset.ToString("0.0")); diff --git a/Plugins/RSBot.General/Components/Accounts.cs b/Plugins/RSBot.General/Components/Accounts.cs index eb9bce32..73f3369b 100644 --- a/Plugins/RSBot.General/Components/Accounts.cs +++ b/Plugins/RSBot.General/Components/Accounts.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -25,23 +25,6 @@ internal class Accounts /// public static Account Joined { get; set; } - /// - /// Get the data file path - /// - private static string _filePath => - Path.Combine(Kernel.BasePath, "User", ProfileManager.SelectedProfile, "autologin.data"); - - /// - /// Check the saving directory - /// - /// - private static void EnsureDirectoryExists() - { - var directory = Path.GetDirectoryName(_filePath); - - Directory.CreateDirectory(directory); - } - /// /// Loads this instance. /// @@ -49,28 +32,10 @@ public static void Load() { try { - EnsureDirectoryExists(); - - SavedAccounts = new List(); - - if (!File.Exists(_filePath)) - return; - - var buffer = File.ReadAllBytes(_filePath); - if (buffer.Length == 0) - return; - - //Decode credentials - var blowfish = new Blowfish(); - buffer = blowfish.Decode(buffer); - - var serialized = Encoding.UTF8.GetString(buffer).Trim('\0'); - - SavedAccounts = JsonSerializer.Deserialize>(serialized) ?? new List(4); + SavedAccounts = GlobalConfig.Get>("Accounts") ?? new List(4); } catch (Exception ex) { - Log.NotifyLang("FileNotFound", _filePath); Log.Fatal(ex); } } @@ -80,25 +45,17 @@ public static void Load() /// public static void Save() { - EnsureDirectoryExists(); - if (SavedAccounts == null) return; try { - //Encode user credentials - var buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(SavedAccounts)); - - //Maybe add some password protection and use blowfish.initialize(password) - var blowfish = new Blowfish(); - buffer = blowfish.Encode(buffer); - - File.WriteAllBytes(_filePath, buffer); + GlobalConfig.Set("Accounts", SavedAccounts); + GlobalConfig.Save(); } - catch + catch (Exception ex) { - Log.NotifyLang("FileNotFound", _filePath); + Log.Fatal(ex); } } } diff --git a/Plugins/RSBot.General/Components/AutoLogin.cs b/Plugins/RSBot.General/Components/AutoLogin.cs index 5c0f567f..e79335cc 100644 --- a/Plugins/RSBot.General/Components/AutoLogin.cs +++ b/Plugins/RSBot.General/Components/AutoLogin.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text; using System.Threading; @@ -51,9 +51,11 @@ public static async void Handle() return; } - var selectedAccount = Accounts.SavedAccounts?.Find(p => - p.Username == GlobalConfig.Get("RSBot.General.AutoLoginAccountUsername") - ); + var autoLoginUsername = !string.IsNullOrEmpty(ProfileManager.SelectedAccount) + ? ProfileManager.SelectedAccount + : GlobalConfig.Get("RSBot.General.AutoLoginAccountUsername"); + + var selectedAccount = Accounts.SavedAccounts?.Find(p => p.Username == autoLoginUsername); if (selectedAccount == null) { _busy = false; @@ -264,7 +266,7 @@ public static void EnterGame(string character) packet.WriteString(character); PacketManager.SendPacket(packet, PacketDestination.Server); - PlayerConfig.Load(character); + RSBot.Core.Config.LoadPlayer(character); EventManager.FireEvent("OnEnterGame"); } diff --git a/Plugins/RSBot.Log/Views/Main.cs b/Plugins/RSBot.Log/Views/Main.cs index 32cf1bd8..5457aa45 100644 --- a/Plugins/RSBot.Log/Views/Main.cs +++ b/Plugins/RSBot.Log/Views/Main.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.IO; using System.Windows.Forms; @@ -50,7 +50,9 @@ public void AppendLog(string message, LogLevel level = LogLevel.Notify) Kernel.BasePath, "User", "Logs", - Game.Player == null ? "Environment" : Game.Player.Name, + Game.Player == null + ? "Environment" + : $"{RSBot.Core.Components.ProfileManager.SelectedProfile}-{Game.Player.Name}", $"{DateTime.Now:dd-MM-yyyy}.txt" ); diff --git a/Plugins/RSBot.Python/Components/API/Core/Config/API.Config.cs b/Plugins/RSBot.Python/Components/API/Core/Config/API.Config.cs index ced77b78..7e9f228a 100644 --- a/Plugins/RSBot.Python/Components/API/Core/Config/API.Config.cs +++ b/Plugins/RSBot.Python/Components/API/Core/Config/API.Config.cs @@ -1,7 +1,8 @@ -using System.IO; +using System.IO; using System.Windows.Forms; using Python.Runtime; using RSBot.Core; +using RSBot.Core.Components; using RSBot.Python.Components.API.Interface; using RSBot.Python.Views; @@ -51,7 +52,7 @@ private string GetLogPath() { return Path.Combine(projectDir, "User", "Logs"); } - return Path.Combine(projectDir, "User", "Logs", Game.Player.Name); + return Path.Combine(projectDir, "User", "Logs", $"{ProfileManager.SelectedProfile}-{Game.Player.Name}"); } public string get_config_dir() diff --git a/Plugins/RSBot.Python/Components/API/Core/Training/API.Training.cs b/Plugins/RSBot.Python/Components/API/Core/Training/API.Training.cs index 38fe2aac..8625b5a1 100644 --- a/Plugins/RSBot.Python/Components/API/Core/Training/API.Training.cs +++ b/Plugins/RSBot.Python/Components/API/Core/Training/API.Training.cs @@ -1,4 +1,4 @@ -using System; +using System; using Python.Runtime; using RSBot.Core; using RSBot.Core.Event; @@ -65,7 +65,7 @@ private bool SetTrainingPosition(float x, float y, ushort region, int radius) var area = Kernel.Bot.Botbase.Area; Position pos = new(x, y, region); - PlayerConfig.Set("RSBot.Area.Region", pos.Region); + PlayerConfig.Set("RSBot.Area.Region", pos.Region.Id); PlayerConfig.Set("RSBot.Area.X", pos.XOffset); PlayerConfig.Set("RSBot.Area.Y", pos.YOffset); PlayerConfig.Set("RSBot.Area.Z", pos.ZOffset); diff --git a/README.md b/README.md index aef7dabd..169ad904 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,8 @@ Run the commands below (You still need MSBuild tooling via Visual Studio): ## Credits -OasisBot was originally developed by [**ngoedde**](https://github.com/ngoedde) (Niklas Gödde/torstmn/Wimbeam) and [**SDClowen**](https://github.com/myildirimofficial) (Mahmut YILDIRIM). This repository is a community-led fork maintained by the Silkroad Developer Community. +| Copyright Owners | Year | Contribution | +| ---------------------------------------------------------------------------------------------------- | ------------ | ------------------ | +| [ngoedde](https://github.com/ngoedde) | 2017-2021 | Original Developer | +| RSBot Team ([ngoedde](https://github.com/ngoedde), [SDClowen](https://github.com/myildirimofficial)) | 2021-2026 | Continuation team | +| [Silkroad Developer Community](https://silkroad-developer-community.github.io/#governance) | 2026-current | Community fork |