From fda5b93473494e769190664e30d87ce42971b15f Mon Sep 17 00:00:00 2001 From: Reetus Date: Tue, 10 Mar 2026 10:39:13 +0700 Subject: [PATCH] Add command-line shard selection and Windows Jump List support Allows launching specific shards directly using a new `--shard` command-line argument. The launcher now tracks the last played date for shards and populates the Windows Taskbar Jump List with recently played entries for quick access. This change also updates several dependencies and adds license headers to modified files. --- ClassicAssist.Launcher/App.xaml.cs | 30 +++- .../ClassicAssist.Launcher.csproj | 7 +- ClassicAssist.Launcher/CommandLineOptions.cs | 10 ++ ClassicAssist.Launcher/MainViewModel.cs | 153 ++++++++++++------ .../Properties/Resources.Designer.cs | 11 +- .../Properties/Resources.resx | 3 + ClassicAssist.Launcher/ShardEntry.cs | 24 ++- ClassicAssist.Launcher/Utility.cs | 2 +- 8 files changed, 178 insertions(+), 62 deletions(-) create mode 100644 ClassicAssist.Launcher/CommandLineOptions.cs diff --git a/ClassicAssist.Launcher/App.xaml.cs b/ClassicAssist.Launcher/App.xaml.cs index 27f18381..41384819 100644 --- a/ClassicAssist.Launcher/App.xaml.cs +++ b/ClassicAssist.Launcher/App.xaml.cs @@ -1,5 +1,25 @@ -using System.Threading; +#region License + +// Copyright (C) 2026 Reetus +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#endregion + +using System.Threading; using System.Windows; +using CommandLine; using Exceptionless; namespace ClassicAssist.Launcher @@ -9,11 +29,15 @@ namespace ClassicAssist.Launcher /// public partial class App : Application { + public static CommandLineOptions CurrentOptions { get; set; } = new CommandLineOptions(); + protected override void OnStartup( StartupEventArgs e ) { - ExceptionlessClient.Default.Configuration.DefaultData.Add( "Locale", - Thread.CurrentThread.CurrentUICulture.Name ); + ExceptionlessClient.Default.Configuration.DefaultData.Add( "Locale", Thread.CurrentThread.CurrentUICulture.Name ); ExceptionlessClient.Default.Startup( "T8v0i7nL90cVRc4sr2pgo5hviThMPRF3OtQ0bK60" ); + + Parser.Default.ParseArguments( e.Args ).WithParsed( o => CurrentOptions = o ); + base.OnStartup( e ); } } diff --git a/ClassicAssist.Launcher/ClassicAssist.Launcher.csproj b/ClassicAssist.Launcher/ClassicAssist.Launcher.csproj index 57f93354..1b3e7cec 100644 --- a/ClassicAssist.Launcher/ClassicAssist.Launcher.csproj +++ b/ClassicAssist.Launcher/ClassicAssist.Launcher.csproj @@ -22,10 +22,11 @@ pdbonly - - + + + - + diff --git a/ClassicAssist.Launcher/CommandLineOptions.cs b/ClassicAssist.Launcher/CommandLineOptions.cs new file mode 100644 index 00000000..f2d77289 --- /dev/null +++ b/ClassicAssist.Launcher/CommandLineOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace ClassicAssist.Launcher +{ + public class CommandLineOptions + { + [Option( "shard", Required = false )] + public string Shard { get; set; } + } +} \ No newline at end of file diff --git a/ClassicAssist.Launcher/MainViewModel.cs b/ClassicAssist.Launcher/MainViewModel.cs index 5d86ba10..8a9a0d3c 100644 --- a/ClassicAssist.Launcher/MainViewModel.cs +++ b/ClassicAssist.Launcher/MainViewModel.cs @@ -1,4 +1,23 @@ -using System; +#region License + +// Copyright (C) 2026 Reetus +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#endregion + +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Configuration; @@ -12,8 +31,10 @@ using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Input; +using System.Windows.Shell; using ClassicAssist.Launcher.Properties; using ClassicAssist.Shared.Misc; +using ClassicAssist.Shared.Resources; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Trinet.Core.IO.Ntfs; @@ -116,7 +137,8 @@ public MainViewModel() HasStatusProtocol = token["HasStatusProtocol"]?.ToObject() ?? true, Encryption = token["Encryption"]?.ToObject() ?? false, Website = token["Website"]?.ToObject(), - IsPreset = true + IsPreset = true, + LastPlayed = token["LastPlayed"]?.ToObject() ?? default }; ShardManager.Shards.AddSorted( shard, new ShardEntryComparer() ); @@ -134,7 +156,8 @@ public MainViewModel() Port = token["Port"]?.ToObject() ?? 2593, Website = token["Website"]?.ToObject(), HasStatusProtocol = token["HasStatusProtocol"]?.ToObject() ?? true, - Encryption = token["Encryption"]?.ToObject() ?? false + Encryption = token["Encryption"]?.ToObject() ?? false, + LastPlayed = token["LastPlayed"]?.ToObject() ?? default }; ShardManager.Shards.AddSorted( shard, new ShardEntryComparer() ); @@ -145,10 +168,7 @@ public MainViewModel() { foreach ( JToken token in config["DeletedPresets"] ) { - ShardEntry shard = new ShardEntry - { - Name = token["Name"]?.ToObject() ?? "Unknown", IsPreset = true - }; + ShardEntry shard = new ShardEntry { Name = token["Name"]?.ToObject() ?? "Unknown", IsPreset = true }; ShardEntry preset = ShardManager.Shards.FirstOrDefault( e => e.Equals( shard ) ); @@ -161,8 +181,7 @@ public MainViewModel() if ( config["SelectedShard"] != null ) { - ShardEntry match = ShardManager.Shards.FirstOrDefault( - s => s.Name == config["SelectedShard"]?.ToObject() ); + ShardEntry match = ShardManager.Shards.FirstOrDefault( s => s.Name == config["SelectedShard"]?.ToObject() ); if ( match != null ) { @@ -185,11 +204,25 @@ public MainViewModel() { CheckPresets().ConfigureAwait( false ); } + + if ( !string.IsNullOrEmpty( App.CurrentOptions.Shard ) ) + { + ShardEntry shard = ShardManager.VisibleShards.FirstOrDefault( e => e.Name == App.CurrentOptions.Shard ); + + if ( shard == null ) + { + MessageBox.Show( Resources.Shard_not_found, Strings.Error ); + return; + } + + SelectedShard = shard; + + StartCommand.Execute( null ); + } } } - public ICommand CheckForUpdateCommand => - _checkforUpdateCommand ?? ( _checkforUpdateCommand = new RelayCommand( CheckForUpdate, UpdaterExists ) ); + public ICommand CheckForUpdateCommand => _checkforUpdateCommand ?? ( _checkforUpdateCommand = new RelayCommand( CheckForUpdate, UpdaterExists ) ); public ClassicOptions ClassicOptions { get; set; } = new ClassicOptions(); @@ -199,8 +232,7 @@ public ObservableCollection ClientPaths set => SetProperty( ref _clientPaths, value ); } - public ICommand ClosingCommand => - _closingCommand ?? ( _closingCommand = new RelayCommand( Closing, o => true ) ); + public ICommand ClosingCommand => _closingCommand ?? ( _closingCommand = new RelayCommand( Closing, o => true ) ); public ObservableCollection DataPaths { @@ -208,16 +240,13 @@ public ObservableCollection DataPaths set => SetProperty( ref _dataPaths, value ); } - public ICommand OptionsCommand => - _optionsCommand ?? ( _optionsCommand = new RelayCommand( ShowOptionsWindow, o => true ) ); + public ICommand OptionsCommand => _optionsCommand ?? ( _optionsCommand = new RelayCommand( ShowOptionsWindow, o => true ) ); public List Plugins { get; set; } = new List(); - public ICommand SelectClientPathCommand => - _selectClientPathCommand ?? ( _selectClientPathCommand = new RelayCommand( SelectClientPath ) ); + public ICommand SelectClientPathCommand => _selectClientPathCommand ?? ( _selectClientPathCommand = new RelayCommand( SelectClientPath ) ); - public ICommand SelectDataPathCommand => - _selectDataPathCommand ?? ( _selectDataPathCommand = new RelayCommand( SelectDataPath ) ); + public ICommand SelectDataPathCommand => _selectDataPathCommand ?? ( _selectDataPathCommand = new RelayCommand( SelectDataPath ) ); public string SelectedClientPath { @@ -243,12 +272,10 @@ public ShardEntry SelectedShard public string ShardsHash { get; set; } = string.Empty; - public ICommand ShowShardsWindowCommand => - _showShardsWindowCommand ?? ( _showShardsWindowCommand = new RelayCommand( ShowShardsWindow, o => true ) ); + public ICommand ShowShardsWindowCommand => _showShardsWindowCommand ?? ( _showShardsWindowCommand = new RelayCommand( ShowShardsWindow, o => true ) ); public ICommand StartCommand => - _startCommand ?? ( _startCommand = new RelayCommandAsync( Start, - o => !string.IsNullOrEmpty( SelectedClientPath ) && !string.IsNullOrEmpty( SelectedDataPath ) ) ); + _startCommand ?? ( _startCommand = new RelayCommandAsync( Start, o => !string.IsNullOrEmpty( SelectedClientPath ) && !string.IsNullOrEmpty( SelectedDataPath ) ) ); private async Task CheckPresets() { @@ -336,8 +363,7 @@ private static void RemoveAlternateDataStreams( string path ) string json = File.ReadAllText( manifestFile ); - IEnumerable manifestEntries = - JsonConvert.DeserializeObject>( json ); + IEnumerable manifestEntries = JsonConvert.DeserializeObject>( json ); if ( manifestEntries == null ) { @@ -364,8 +390,7 @@ private static void RemoveAlternateDataStreams( string path ) private void ReadClassicOptions( JObject config ) { - PropertyInfo[] properties = - typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); + PropertyInfo[] properties = typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); foreach ( PropertyInfo property in properties ) { @@ -426,13 +451,7 @@ private static void CheckForUpdate( object obj ) private void SelectClientPath( object obj ) { - OpenFileDialog ofd = new OpenFileDialog - { - CheckFileExists = true, - Multiselect = false, - Filter = "ClassicUO.exe|ClassicUO.exe", - Title = Resources.Select_a_client - }; + OpenFileDialog ofd = new OpenFileDialog { CheckFileExists = true, Multiselect = false, Filter = "ClassicUO.exe|ClassicUO.exe", Title = Resources.Select_a_client }; bool? result = ofd.ShowDialog(); @@ -451,10 +470,7 @@ private void SelectClientPath( object obj ) private void SelectDataPath( object obj ) { - FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog - { - Description = Resources.Select_your_Ultima_Online_directory, ShowNewFolderButton = false - }; + FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog { Description = Resources.Select_your_Ultima_Online_directory, ShowNewFolderButton = false }; DialogResult result = folderBrowserDialog.ShowDialog(); if ( result != DialogResult.OK ) @@ -487,8 +503,7 @@ private async Task Start( object obj ) StringBuilder args = new StringBuilder(); - List pluginList = - new List { Path.Combine( Environment.CurrentDirectory, "ClassicAssist.dll" ) }; + List pluginList = new List { Path.Combine( Environment.CurrentDirectory, "ClassicAssist.dll" ) }; foreach ( PluginEntry plugin in Plugins ) { @@ -507,8 +522,7 @@ private async Task Start( object obj ) ProcessStartInfo psi = new ProcessStartInfo { - WorkingDirectory = - Path.GetDirectoryName( SelectedClientPath ) ?? throw new InvalidOperationException(), + WorkingDirectory = Path.GetDirectoryName( SelectedClientPath ) ?? throw new InvalidOperationException(), FileName = SelectedClientPath, Arguments = args.ToString(), UseShellExecute = true @@ -516,16 +530,52 @@ private async Task Start( object obj ) Process p = Process.Start( psi ); + SelectedShard.LastPlayed = DateTime.Now; + + UpdateJumpList( SelectedShard.Name ); + if ( p != null && !p.HasExited ) { Application.Current.Shutdown( 0 ); } } + private void UpdateJumpList( string shardName ) + { + ProcessModule processModule = Process.GetCurrentProcess().MainModule; + + if ( processModule == null ) + { + return; + } + + string fileName = processModule.FileName; + string directory = Path.GetDirectoryName( fileName ); + + JumpList jumpList = new JumpList { ShowRecentCategory = true }; + + IOrderedEnumerable playedShards = ShardManager.VisibleShards.Where( e => e.LastPlayed != default ).OrderBy( e => e.LastPlayed ); + + foreach ( ShardEntry shard in playedShards ) + { + jumpList.JumpItems.Add( new JumpTask + { + Title = shard.Name, + Arguments = $"--shard \"{shard.Name}\"", + ApplicationPath = fileName, + IconResourcePath = fileName, + WorkingDirectory = directory, + CustomCategory = "Shards" + } ); + } + + JumpList.SetJumpList( Application.Current, jumpList ); + jumpList.Apply(); + } + private void BuildClassicOptions( StringBuilder args ) { - PropertyInfo[] properties = - typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); + PropertyInfo[] properties = typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); foreach ( PropertyInfo property in properties ) { @@ -537,7 +587,7 @@ private void BuildClassicOptions( StringBuilder args ) continue; } - bool skip = val is bool b && b == false && !attr.IncludeIfFalse; + bool skip = val is bool b && !b && !attr.IncludeIfFalse; bool canInclude = true; if ( !string.IsNullOrEmpty( attr.CanIncludeProperty ) ) @@ -562,8 +612,7 @@ private void ShowShardsWindow( object obj ) ShardsWindow window = new ShardsWindow(); window.ShowDialog(); - if ( !( window.DataContext is ShardsViewModel vm ) || vm.DialogResult != DialogResult.OK || - vm.SelectedShard == null ) + if ( !( window.DataContext is ShardsViewModel vm ) || vm.DialogResult != DialogResult.OK || vm.SelectedShard == null ) { return; } @@ -657,7 +706,8 @@ private void Closing( object obj ) { "Port", shard.Port }, { "HasStatusProtocol", shard.HasStatusProtocol }, { "Website", shard.Website }, - { "Encryption", shard.Encryption } + { "Encryption", shard.Encryption }, + { "LastPlayed", shard.LastPlayed } }; presetsArray.Add( shardObj ); @@ -668,9 +718,7 @@ private void Closing( object obj ) WriteClassicOptions( config ); - using ( JsonTextWriter jtw = - new JsonTextWriter( - new StreamWriter( Path.Combine( Environment.CurrentDirectory, CONFIG_FILENAME ) ) ) ) + using ( JsonTextWriter jtw = new JsonTextWriter( new StreamWriter( Path.Combine( Environment.CurrentDirectory, CONFIG_FILENAME ) ) ) ) { jtw.Formatting = Formatting.Indented; config.WriteTo( jtw ); @@ -679,8 +727,7 @@ private void Closing( object obj ) private void WriteClassicOptions( JObject config ) { - PropertyInfo[] properties = - typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); + PropertyInfo[] properties = typeof( ClassicOptions ).GetProperties( BindingFlags.Public | BindingFlags.Instance ); foreach ( PropertyInfo property in properties ) { diff --git a/ClassicAssist.Launcher/Properties/Resources.Designer.cs b/ClassicAssist.Launcher/Properties/Resources.Designer.cs index f510c3fd..2d8d0613 100644 --- a/ClassicAssist.Launcher/Properties/Resources.Designer.cs +++ b/ClassicAssist.Launcher/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace ClassicAssist.Launcher.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -330,6 +330,15 @@ public static string Shard { } } + /// + /// Looks up a localized string similar to Shard not found. + /// + public static string Shard_not_found { + get { + return ResourceManager.GetString("Shard not found", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shards. /// diff --git a/ClassicAssist.Launcher/Properties/Resources.resx b/ClassicAssist.Launcher/Properties/Resources.resx index 29be225e..63308b42 100644 --- a/ClassicAssist.Launcher/Properties/Resources.resx +++ b/ClassicAssist.Launcher/Properties/Resources.resx @@ -222,4 +222,7 @@ Moving item {0} / {1} + + Shard not found + \ No newline at end of file diff --git a/ClassicAssist.Launcher/ShardEntry.cs b/ClassicAssist.Launcher/ShardEntry.cs index 7cab4f85..c86292aa 100644 --- a/ClassicAssist.Launcher/ShardEntry.cs +++ b/ClassicAssist.Launcher/ShardEntry.cs @@ -1,4 +1,18 @@ -using System; +#region License + +// Copyright (C) 2026 Reetus +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY + +#endregion + +using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; @@ -13,6 +27,7 @@ public class ShardEntry : INotifyPropertyChanged, IEquatable private string _address; private bool _deleted; private bool _encryption; + private DateTime _lastPlayed; private string _name; private string _ping; private int _port; @@ -48,6 +63,13 @@ public bool Encryption [JsonIgnore] public bool IsPreset { get; set; } + [JsonProperty( "last_played" )] + public DateTime LastPlayed + { + get => _lastPlayed; + set => SetProperty( ref _lastPlayed, value ); + } + [JsonProperty( "name" )] public string Name { diff --git a/ClassicAssist.Launcher/Utility.cs b/ClassicAssist.Launcher/Utility.cs index 66d141b9..f7486bfe 100644 --- a/ClassicAssist.Launcher/Utility.cs +++ b/ClassicAssist.Launcher/Utility.cs @@ -1,4 +1,4 @@ -#region License +#region License // Copyright (C) 2020 Reetus //