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 //