diff options
| author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-14 12:21:40 -0400 |
|---|---|---|
| committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2018-08-14 12:21:40 -0400 |
| commit | 4f28ea33bd7cc65485402c5e85259083e86b49e1 (patch) | |
| tree | 86c4d8f9272de9a715cfcbf4008f0c09f5a59a21 | |
| parent | 60b41195778af33fd609eab66d9ae3f1d1165e8f (diff) | |
| parent | 4dd4efc96fac6a7ab66c14edead10e4fa988040d (diff) | |
| download | SMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.tar.gz SMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.tar.bz2 SMAPI-4f28ea33bd7cc65485402c5e85259083e86b49e1.zip | |
Merge branch 'develop' into stable
30 files changed, 754 insertions, 435 deletions
diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index bc0ddf69..2e4f9373 100644 --- a/build/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -1,5 +1,5 @@ using System.Reflection; [assembly: AssemblyProduct("SMAPI")] -[assembly: AssemblyVersion("2.6.0")] -[assembly: AssemblyFileVersion("2.6.0")] +[assembly: AssemblyVersion("2.7.0")] +[assembly: AssemblyFileVersion("2.7.0")] diff --git a/docs/release-notes.md b/docs/release-notes.md index e4f9fd1d..133006e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,33 @@ # Release notes +## 2.7 +* For players: + * Updated for Stardew Valley 1.3.28. + * Improved how mod issues are listed in the console and log. + * Revamped installer. It now... + * uses a new format that should be more intuitive; + * lets players on Linux/Mac choose the console color scheme (SMAPI will auto-detect it on Windows); + * and validates requirements earlier. + * Fixed custom festival maps always using spring tilesheets. + * Fixed `player_add` command not recognising return scepter. + * Fixed `player_add` command showing fish twice. + * Fixed some SMAPI logs not deleted when starting a new session. + +* For modders: + * Added support for `.json` data files in the content API (including Content Patcher). + * Added propagation for asset changes through the content API for... + * child sprites; + * dialogue; + * map tilesheets. + * Added `--mods-path` CLI command-line argument to switch between mod folders. + * All enums are now JSON-serialised by name instead of numeric value. (Previously only a few enums were serialised that way. JSON files which already have numeric enum values will still be parsed fine.) + * Fixed false compatibility error when constructing multidimensional arrays. + * Fixed `.ToSButton()` methods not being public. + * Updated compatibility list. + +* For SMAPI developers: + * Dropped support for pre-SMAPI-2.6 update checks in the web API. + _These are no longer useful, even if the player still has earlier versions of SMAPI. Older versions of SMAPI won't launch in Stardew Valley 1.3 (so they won't check for updates), and newer versions of SMAPI/mods won't work with older versions of the game._ + ## 2.6 * For players: * Updated for Stardew Valley 1.3. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index d829baf9..ed45871a 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -138,6 +138,7 @@ change without warning. argument | purpose -------- | ------- `--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) +`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. ### Compile flags SMAPI uses a small number of conditional compilation constants, which you can set by editing the diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs new file mode 100644 index 00000000..d212876a --- /dev/null +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -0,0 +1,61 @@ +using System.IO; + +namespace StardewModdingAPI.Installer.Framework +{ + /// <summary>Manages paths for the SMAPI installer.</summary> + internal class InstallerPaths + { + /********* + ** Accessors + *********/ + /// <summary>The directory containing the installer files for the current platform.</summary> + public DirectoryInfo PackageDir { get; } + + /// <summary>The directory containing the installed game.</summary> + public DirectoryInfo GameDir { get; } + + /// <summary>The directory into which to install mods.</summary> + public DirectoryInfo ModsDir { get; } + + /// <summary>The full path to the directory containing the installer files for the current platform.</summary> + public string PackagePath => this.PackageDir.FullName; + + /// <summary>The full path to the directory containing the installed game.</summary> + public string GamePath => this.GameDir.FullName; + + /// <summary>The full path to the directory into which to install mods.</summary> + public string ModsPath => this.ModsDir.FullName; + + /// <summary>The full path to the installed SMAPI executable file.</summary> + public string ExecutablePath { get; } + + /// <summary>The full path to the vanilla game launcher on Linux/Mac.</summary> + public string UnixLauncherPath { get; } + + /// <summary>The full path to the installed SMAPI launcher on Linux/Mac before it's renamed.</summary> + public string UnixSmapiLauncherPath { get; } + + /// <summary>The full path to the vanilla game launcher on Linux/Mac after SMAPI is installed.</summary> + public string UnixBackupLauncherPath { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="packageDir">The directory path containing the installer files for the current platform.</param> + /// <param name="gameDir">The directory path for the installed game.</param> + /// <param name="gameExecutableName">The name of the game's executable file for the current platform.</param> + public InstallerPaths(DirectoryInfo packageDir, DirectoryInfo gameDir, string gameExecutableName) + { + this.PackageDir = packageDir; + this.GameDir = gameDir; + this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods")); + + this.ExecutablePath = Path.Combine(gameDir.FullName, gameExecutableName); + this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley"); + this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); + this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original"); + } + } +} diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index f39486e1..0aac1da2 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; +using StardewModdingAPI.Installer.Framework; using StardewModdingAPI.Internal; using StardewModdingAPI.Internal.ConsoleWriting; @@ -135,7 +136,7 @@ namespace StardewModdingApi.Installer } /// <summary>Handles writing color-coded text to the console.</summary> - private readonly ColorfulConsoleWriter ConsoleWriter; + private ColorfulConsoleWriter ConsoleWriter; /********* @@ -168,6 +169,9 @@ namespace StardewModdingApi.Installer /// </remarks> public void Run(string[] args) { + /********* + ** Step 1: initial setup + *********/ /**** ** Get platform & set window title ****/ @@ -175,6 +179,9 @@ namespace StardewModdingApi.Installer Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); + /**** + ** Check if correct installer + ****/ #if SMAPI_FOR_WINDOWS if (platform == Platform.Linux || platform == Platform.Mac) { @@ -182,78 +189,17 @@ namespace StardewModdingApi.Installer Console.ReadLine(); return; } -#endif - - /**** - ** read command-line arguments - ****/ - // get action from CLI - bool installArg = args.Contains("--install"); - bool uninstallArg = args.Contains("--uninstall"); - if (installArg && uninstallArg) - { - this.PrintError("You can't specify both --install and --uninstall command-line flags."); - Console.ReadLine(); - return; - } - - // get game path from CLI - string gamePathArg = null; - { - int pathIndex = Array.LastIndexOf(args, "--game-path") + 1; - if (pathIndex >= 1 && args.Length >= pathIndex) - gamePathArg = args[pathIndex]; - } - - /**** - ** collect details - ****/ - // get game path - DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg); - if (installDir == null) - { - this.PrintError("Failed finding your game path."); - Console.ReadLine(); - return; - } - - // get folders - DirectoryInfo packageDir = platform.IsMono() - ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono - : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows")); - DirectoryInfo modsDir = new DirectoryInfo(Path.Combine(installDir.FullName, "Mods")); - var paths = new - { - executable = Path.Combine(installDir.FullName, EnvironmentUtility.GetExecutableName(platform)), - unixSmapiLauncher = Path.Combine(installDir.FullName, "StardewModdingAPI"), - unixLauncher = Path.Combine(installDir.FullName, "StardewValley"), - unixLauncherBackup = Path.Combine(installDir.FullName, "StardewValley-original") - }; - - // show output - this.PrintInfo($"Your game folder: {installDir}."); - - /**** - ** validate assumptions - ****/ - if (!packageDir.Exists) - { - this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip") - ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." - : $"The 'internal/{packageDir.Name}' package folder is missing (should be at {packageDir})." - ); - Console.ReadLine(); - return; - } - if (!File.Exists(paths.executable)) +#else + if (platform == Platform.Windows) { - this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); + this.PrintError($"This is the installer for Linux/Mac. Run the 'install on Windows.exe' file instead."); Console.ReadLine(); return; } +#endif /**** - ** validate Windows dependencies + ** Check Windows dependencies ****/ if (platform == Platform.Windows) { @@ -276,159 +222,324 @@ namespace StardewModdingApi.Installer } } - Console.WriteLine(); - /**** - ** ask user what to do + ** read command-line arguments ****/ - ScriptAction action; + // get action from CLI + bool installArg = args.Contains("--install"); + bool uninstallArg = args.Contains("--uninstall"); + if (installArg && uninstallArg) + { + this.PrintError("You can't specify both --install and --uninstall command-line flags."); + Console.ReadLine(); + return; + } - if (installArg) - action = ScriptAction.Install; - else if (uninstallArg) - action = ScriptAction.Uninstall; - else + // get game path from CLI + string gamePathArg = null; + { + int pathIndex = Array.LastIndexOf(args, "--game-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) + gamePathArg = args[pathIndex]; + } + + + /********* + ** Step 2: choose a theme (can't auto-detect on Linux/Mac) + *********/ + MonitorColorScheme scheme = MonitorColorScheme.AutoDetect; + if (platform == Platform.Linux || platform == Platform.Mac) { - this.PrintInfo("You can...."); - this.PrintInfo("[1] Install SMAPI."); - this.PrintInfo("[2] Uninstall SMAPI."); + /**** + ** print header + ****/ + this.PrintPlain("Hi there! I'll help you install or remove SMAPI. Just a few questions first."); + this.PrintPlain("----------------------------------------------------------------------------"); + Console.WriteLine(); + + /**** + ** show theme selector + ****/ + // get theme writers + var lightBackgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.LightBackground); + var darkDarkgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.DarkBackground); + + // print question + this.PrintPlain("Which text looks more readable?"); + Console.WriteLine(); + Console.Write(" [1] "); + lightBackgroundWriter.WriteLine("Dark text on light background", ConsoleLogLevel.Info); + Console.Write(" [2] "); + darkDarkgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info); Console.WriteLine(); - string choice = this.InteractivelyChoose("What do you want to do? Type 1 or 2, then press enter.", "1", "2"); + // handle choice + string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }); switch (choice) { case "1": - action = ScriptAction.Install; + scheme = MonitorColorScheme.LightBackground; + this.ConsoleWriter = lightBackgroundWriter; break; case "2": - action = ScriptAction.Uninstall; + scheme = MonitorColorScheme.DarkBackground; + this.ConsoleWriter = darkDarkgroundWriter; break; default: throw new InvalidOperationException($"Unexpected action key '{choice}'."); } - Console.WriteLine(); } + Console.Clear(); - /**** - ** Always uninstall old files - ****/ - // restore game launcher - if (platform.IsMono() && File.Exists(paths.unixLauncherBackup)) - { - this.PrintDebug("Removing SMAPI launcher..."); - this.InteractivelyDelete(paths.unixLauncher); - File.Move(paths.unixLauncherBackup, paths.unixLauncher); - } - // remove old files - string[] removePaths = this.GetUninstallPaths(installDir, modsDir) - .Where(path => Directory.Exists(path) || File.Exists(path)) - .ToArray(); - if (removePaths.Any()) + /********* + ** Step 3: find game folder + *********/ + InstallerPaths paths; { - this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); - foreach (string path in removePaths) - this.InteractivelyDelete(path); + /**** + ** print header + ****/ + this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just a few questions first."); + this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); + this.PrintDebug("----------------------------------------------------------------------------"); + Console.WriteLine(); + + /**** + ** collect details + ****/ + // get game path + this.PrintInfo("Where is your game folder?"); + DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg); + if (installDir == null) + { + this.PrintError("Failed finding your game path."); + Console.ReadLine(); + return; + } + + // get folders + DirectoryInfo packageDir = platform.IsMono() + ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono + : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows")); + paths = new InstallerPaths(packageDir, installDir, EnvironmentUtility.GetExecutableName(platform)); } + Console.Clear(); - /**** - ** Install new files - ****/ - if (action == ScriptAction.Install) + + /********* + ** Step 4: validate assumptions + *********/ { - // copy SMAPI files to game dir - this.PrintDebug("Adding SMAPI files..."); - foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + if (!paths.PackageDir.Exists) { - if (sourceFile.Name == this.InstallerFileName) - continue; + this.PrintError(platform == Platform.Windows && paths.PackagePath.Contains(Path.GetTempPath()) && paths.PackagePath.Contains(".zip") + ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." + : $"The 'internal/{paths.PackageDir.Name}' package folder is missing (should be at {paths.PackagePath})." + ); + Console.ReadLine(); + return; + } - string targetPath = Path.Combine(installDir.FullName, sourceFile.Name); - this.InteractivelyDelete(targetPath); - sourceFile.CopyTo(targetPath); + if (!File.Exists(paths.ExecutablePath)) + { + this.PrintError("The detected game install path doesn't contain a Stardew Valley executable."); + Console.ReadLine(); + return; } + } + + + /********* + ** Step 5: ask what to do + *********/ + ScriptAction action; + { + /**** + ** print header + ****/ + this.PrintInfo("Hi there! I'll help you install or remove SMAPI. Just one question first."); + this.PrintDebug($"Game path: {paths.GamePath}"); + this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); + this.PrintDebug("----------------------------------------------------------------------------"); + Console.WriteLine(); - // replace mod launcher (if possible) - if (platform.IsMono()) + /**** + ** ask what to do + ****/ + if (installArg) + action = ScriptAction.Install; + else if (uninstallArg) + action = ScriptAction.Uninstall; + else { - this.PrintDebug("Safely replacing game launcher..."); - if (File.Exists(paths.unixLauncher)) + this.PrintInfo("What do you want to do?"); + Console.WriteLine(); + this.PrintInfo("[1] Install SMAPI."); + this.PrintInfo("[2] Uninstall SMAPI."); + Console.WriteLine(); + + string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }); + switch (choice) { - if (!File.Exists(paths.unixLauncherBackup)) - File.Move(paths.unixLauncher, paths.unixLauncherBackup); - else - this.InteractivelyDelete(paths.unixLauncher); + case "1": + action = ScriptAction.Install; + break; + case "2": + action = ScriptAction.Uninstall; + break; + default: + throw new InvalidOperationException($"Unexpected action key '{choice}'."); } + } + } + Console.Clear(); + - File.Move(paths.unixSmapiLauncher, paths.unixLauncher); + /********* + ** Step 6: apply + *********/ + { + /**** + ** print header + ****/ + this.PrintInfo($"That's all I need! I'll {action.ToString().ToLower()} SMAPI now."); + this.PrintDebug($"Game path: {paths.GamePath}"); + this.PrintDebug($"Color scheme: {this.GetDisplayText(scheme)}"); + this.PrintDebug("----------------------------------------------------------------------------"); + Console.WriteLine(); + + /**** + ** Always uninstall old files + ****/ + // restore game launcher + if (platform.IsMono() && File.Exists(paths.UnixBackupLauncherPath)) + { + this.PrintDebug("Removing SMAPI launcher..."); + this.InteractivelyDelete(paths.UnixLauncherPath); + File.Move(paths.UnixBackupLauncherPath, paths.UnixLauncherPath); } - // create mods directory (if needed) - if (!modsDir.Exists) + // remove old files + string[] removePaths = this.GetUninstallPaths(paths.GameDir, paths.ModsDir) + .Where(path => Directory.Exists(path) || File.Exists(path)) + .ToArray(); + if (removePaths.Any()) { - this.PrintDebug("Creating mods directory..."); - modsDir.Create(); + this.PrintDebug(action == ScriptAction.Install ? "Removing previous SMAPI files..." : "Removing SMAPI files..."); + foreach (string path in removePaths) + this.InteractivelyDelete(path); } - // add or replace bundled mods - modsDir.Create(); - DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(packageDir.FullName, "Mods")); - if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) + /**** + ** Install new files + ****/ + if (action == ScriptAction.Install) { - this.PrintDebug("Adding bundled mods..."); + // copy SMAPI files to game dir + this.PrintDebug("Adding SMAPI files..."); + foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile)) + { + if (sourceFile.Name == this.InstallerFileName) + continue; + + string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name); + this.InteractivelyDelete(targetPath); + sourceFile.CopyTo(targetPath); + } - // special case: rename Omegasis' SaveBackup mod + // replace mod launcher (if possible) + if (platform.IsMono()) { - DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "SaveBackup")); - DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(modsDir.FullName, "AdvancedSaveBackup")); - FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json")); - if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1)) + this.PrintDebug("Safely replacing game launcher..."); + if (File.Exists(paths.UnixLauncherPath)) { - this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}..."); - this.Move(oldFolder, newFolder.FullName); + if (!File.Exists(paths.UnixBackupLauncherPath)) + File.Move(paths.UnixLauncherPath, paths.UnixBackupLauncherPath); + else + this.InteractivelyDelete(paths.UnixLauncherPath); } + + File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath); } - // add bundled mods - foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) + // create mods directory (if needed) + if (!paths.ModsDir.Exists) { - this.PrintDebug($" adding {sourceDir.Name}..."); + this.PrintDebug("Creating mods directory..."); + paths.ModsDir.Create(); + } + + // add or replace bundled mods + DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(paths.PackageDir.FullName, "Mods")); + if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any()) + { + this.PrintDebug("Adding bundled mods..."); - // init/clear target dir - DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(modsDir.FullName, sourceDir.Name)); - if (targetDir.Exists) + // special case: rename Omegasis' SaveBackup mod { - this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles); - foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos()) + DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "SaveBackup")); + DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "AdvancedSaveBackup")); + FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json")); + if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1)) { - if (protectedFiles == null || !protectedFiles.Contains(entry.Name)) - this.InteractivelyDelete(entry.FullName); + this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}..."); + this.Move(oldFolder, newFolder.FullName); } } - else - targetDir.Create(); - // copy files - foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) - sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); + // add bundled mods + foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories()) + { + this.PrintDebug($" adding {sourceDir.Name}..."); + + // init/clear target dir + DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name)); + if (targetDir.Exists) + { + this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet<string> protectedFiles); + foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos()) + { + if (protectedFiles == null || !protectedFiles.Contains(entry.Name)) + this.InteractivelyDelete(entry.FullName); + } + } + else + targetDir.Create(); + + // copy files + foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile)) + sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name)); |
