diff --git a/diff-text/diff-text.csproj b/diff-text/diff-text.csproj index 1eb1248..7461e1c 100644 --- a/diff-text/diff-text.csproj +++ b/diff-text/diff-text.csproj @@ -16,6 +16,6 @@ - + diff --git a/paradox-mod-merger/Diff.cs b/paradox-mod-merger/Diff.cs index a8d4ef4..f4f01da 100644 --- a/paradox-mod-merger/Diff.cs +++ b/paradox-mod-merger/Diff.cs @@ -7,14 +7,13 @@ using DTLib.Ben.Demystifier; namespace ParadoxModMerger; -public record struct ConflictingModFile(string FilePath, string[] Mods); - public enum DiffState { Added, Equal, Removed, Changed } public record struct DiffPart(T Value, DiffState State); +public record struct ConflictingModFile(string FilePath, string[] Mods); static class Diff { @@ -23,7 +22,7 @@ static class Diff public static void DiffCommandHandler(string connected_pathes) { - IOPath[] moddirs = Program.SplitStringToPaths(connected_pathes); + IOPath[] moddirs = Program.SplitArgToPaths(connected_pathes, false); var conflicts = FindModConflicts(moddirs); LogModConflicts(conflicts); } @@ -148,14 +147,14 @@ static class Diff case ConsoleKey.LeftArrow: { ColoredConsole.Write("w", "enter left mod number: "); - string answ = Console.ReadLine(); + string answ = Console.ReadLine()!; selected_mod0_i = answ.ToInt(); break; } case ConsoleKey.RightArrow: { ColoredConsole.Write("w", "enter right mod number: "); - string answ = Console.ReadLine(); + string answ = Console.ReadLine()!; selected_mod1_i = answ.ToInt(); break; } @@ -194,7 +193,7 @@ static class Diff public static IEnumerable> DiffCollections(ICollection col0, ICollection col1) { - foreach (T el in col0) + foreach (var el in col0) yield return new DiffPart(el, col1.Contains(el) ? DiffState.Equal : DiffState.Removed); foreach (var el in col1) if (!col0.Contains(el)) diff --git a/paradox-mod-merger/Localisation.cs b/paradox-mod-merger/Localisation.cs index 513bbf7..26e2212 100644 --- a/paradox-mod-merger/Localisation.cs +++ b/paradox-mod-merger/Localisation.cs @@ -23,4 +23,52 @@ static class Localisation else Log("y", $"file {rusFileName} already exists"); } } + + // deletes all localisations except l_russian and l_english + public static void Clean(IOPath _loc_dir) + { + Log("g", $"deleted {RemoveUnneededDirs(_loc_dir)} dirs"); + Log("g", $"deleted {RemoveUnneededFiles(_loc_dir)} files"); + + + int RemoveUnneededDirs(IOPath loc_dir) + { + int count = 0; + foreach (var subdir in Directory.GetDirectories(loc_dir)) + { + string dir_basename = subdir.LastName().Str; + if (dir_basename == "russian" || dir_basename == "english") + continue; + + if (dir_basename == "replace") + { + RemoveUnneededDirs(subdir); + RemoveUnneededFiles(subdir); + continue; + } + + if (dir_basename.Contains("rus")) + Log("y", $"unexpected dir: {subdir}"); + Directory.Delete(subdir); + count++; + } + + return count; + } + + int RemoveUnneededFiles(IOPath loc_dir) + { + int count = 0; + foreach (var file in Directory.GetFiles(loc_dir)) + { + if(file.EndsWith("l_russian") || file.EndsWith("l_enghish")) + continue; + if (!file.Contains("_l_") || !file.EndsWith(".yml")) + Log("y",$"unexpected file: {file}"); + File.Delete(file); + count++; + } + return count; + } + } } \ No newline at end of file diff --git a/paradox-mod-merger/Merge.cs b/paradox-mod-merger/Merge.cs index 3b13225..2f9d19d 100644 --- a/paradox-mod-merger/Merge.cs +++ b/paradox-mod-merger/Merge.cs @@ -56,18 +56,22 @@ static class Merge File.AppendAllText(out_modlist_file, $"{moddir.LastName()}\n"); } - public static void ConsoleAskYN(string question, Action yes, Action no) + public static void ConsoleAskYN(string question, Action? yes, Action? no) { Log("y", question + " [y/n]"); - string answ = ColoredConsole.Read("w"); - if (answ == "y") yes(); - else no(); + string answ = ColoredConsole.Read("w").ToLower(); + if (answ == "y") yes?.Invoke(); + else no?.Invoke(); } static void HandleConflicts(IOPath[] moddirs) { var conflicts = Diff.FindModConflicts(moddirs); - if (conflicts.Count <= 0) return; + conflicts = conflicts.Where(cfl => + !cfl.FilePath.EndsWith("descriptor.mod") && + !cfl.FilePath.EndsWith(modlist_filename)).ToList(); + if (conflicts.Count < 1) + return; Diff.LogModConflicts(conflicts); ConsoleAskYN("continue merge?", @@ -80,4 +84,117 @@ static class Merge () => {}); }); } + + + public static void UpdateMods(IOPath updated_mods_dir, IOPath[] outdated_dirs, IOPath backup_dir) + { + var src_dir_mods = Directory.GetDirectories(updated_mods_dir).ToList(); + List not_found_mods = new List(); + List changed_mods = new List(); + List unchanged_mods = new List(); + + foreach (IOPath outdated_mods_dir in outdated_dirs) + foreach (var mod in Directory.GetDirectories(outdated_mods_dir)) + { + Log("b", "updating mod ", "c", mod.LastName().Str); + var mod_updated = mod.ReplaceBase(outdated_mods_dir, updated_mods_dir); + int updated_index = src_dir_mods.IndexOf(mod_updated); // IOPath comparison doesnt work? + + if (updated_index == -1) + { + Log("m", $"mod {mod.LastName()} not found in {updated_mods_dir}"); + not_found_mods.Add(mod); + continue; + } + + var diff = Diff.DiffDirs(mod, mod_updated) + .Where(d => d.State != DiffState.Equal) + .ToList(); + if (!diff.Any()) + { + Log("gray","unchanged"); + unchanged_mods.Add(mod); + continue; + } + + Log("file difference:"); + foreach (var fileD in diff) + { + string color = fileD.State switch + { + DiffState.Added => "g", + DiffState.Changed => "y", + DiffState.Removed => "m", + _ => throw new ArgumentOutOfRangeException() + }; + Log(color, fileD.Value.Str); + } + + ConsoleAskYN("replace mod with its updated version?", + () => + { + var mod_backup = Path.Concat(backup_dir, mod.LastName()); + Log($"moving {mod} to {backup_dir}"); + Directory.Move(mod, mod_backup, false); + Log($"copying {mod_updated} to {outdated_mods_dir}"); + Directory.Copy(mod_updated, mod, false); + Log("g", $"mod {mod.LastName()} updated"); + changed_mods.Add(mod); + }, + ()=>unchanged_mods.Add(mod)); + } + + List added_mods = new List(src_dir_mods.Count - changed_mods.Count); + var found_mods = Enumerable + .Concat(changed_mods, unchanged_mods) + .Select(m=>Path.Concat(updated_mods_dir, m.LastName())) + .ToList(); + foreach (var modD in Diff.DiffCollections(found_mods, src_dir_mods)) + { + if (modD.State == DiffState.Added) + added_mods.Add(modD.Value); + } + + Log("b", "mod update summary:"); + if (added_mods.Count > 0) + Log("w", $"added {added_mods.Count}:\n", + "g", added_mods.MergeToString('\n')); + if (changed_mods.Count > 0) + Log("w", $"changed {changed_mods.Count}:\n", + "y", changed_mods.MergeToString('\n')); + if (not_found_mods.Count > 0) + Log("w", $"not found {not_found_mods.Count}:\n", + "m", not_found_mods.MergeToString('\n')); + if (unchanged_mods.Count>0) + Log("w", $"unchanged {unchanged_mods.Count}"); + } + + public static void RenameModsCommandHandler(string dir_with_mods, string rename_pairs_str) + { + string[] split = Program.SplitArgToStrings(rename_pairs_str, false); + int rename_pairs_length = split.Length / 2; + if (split.Length % 2 != 0) + throw new Exception($"rename_pairs length is not even ({rename_pairs_length})"); + var rename_pairs = new (string old_name, string new_name)[rename_pairs_length]; + for (int i = 0; i < rename_pairs_length; i++) + { + rename_pairs[i] = (split[i * 2], split[i * 2 + 1]); + } + + RenameMods(dir_with_mods, rename_pairs); + } + + public static void RenameMods(IOPath dir_with_mods, (string old_name,string new_name)[] rename_pairs) + { + foreach (var re in rename_pairs) + { + var old_mod_path = Path.Concat(dir_with_mods, re.old_name); + var new_mod_path = Path.Concat(dir_with_mods, re.new_name); + if (Directory.Exists(old_mod_path)) + { + Log("b","renaming ", "c", re.old_name, "b", " to ", "c", re.new_name); + Directory.Move(old_mod_path, new_mod_path, false); + } + } + } } \ No newline at end of file diff --git a/paradox-mod-merger/Program.cs b/paradox-mod-merger/Program.cs index 8e2b473..aa43e86 100644 --- a/paradox-mod-merger/Program.cs +++ b/paradox-mod-merger/Program.cs @@ -6,6 +6,8 @@ global using DTLib; global using DTLib.Extensions; global using DTLib.Filesystem; global using DTLib.Logging; +using System.Linq; +using System.Security.Cryptography; using DTLib.Console; namespace ParadoxModMerger; @@ -15,7 +17,7 @@ public static class Program static ConsoleLogger logger = new($"logs", "main"); static void Log(params string[] msg) => logger.Log(msg); - static void Main(string[] args) + static int Main(string[] args) { try { @@ -25,47 +27,62 @@ public static class Program string outPath = "" ; new LaunchArgumentParser( - new LaunchArgument(new []{"o", "out"}, - "sets output path", - p => outPath=p, - "out_path", - 0), - new LaunchArgument(new []{"clear"}, - "Clear mod files and put them into separate dirs in output dir. Requires -o", - wdir=>Workshop.ClearWorkshop(wdir, outPath), - "workshop_dir", - 1), - new LaunchArgument(new []{"diff"}, - "Compares mod files by hash", - p=>Diff.DiffCommandHandler(p), - "first_mod_directory:second_mod_directory:...", - 1), - new LaunchArgument(new []{"diff-detailed"}, - "reads conflicts_XXX.dtsod file and shows text diff for each file", - p=>Diff.DiffDetailedCommandHandler(p), - "conflicts_dtsod_path", - 1 - ), - new LaunchArgument(new []{"merge-subdirs"}, - "Merges mods and shows conflicts. Requires -o", - d => Merge.MergeAll(Directory.GetDirectories(d), outPath), - "dir_with_mods", - 1), - new LaunchArgument(new []{"merge-into", "merge-single"}, - "Merges one mod into output dir and shows conflicts. Requires -o", - mod=>Merge.MergeInto(mod, outPath), - "mod_dir", - 1), - new LaunchArgument(new []{"gen-rus-locale"}, - "Creates l_russian copy of english locale in output directory. Requires -o", - eng=>Localisation.GenerateRussian(eng, outPath), - "english_locale_path", - 1), - new LaunchArgument(new []{"desc"}, - "Downloads mod description from steam to new file in outDir. Requires -o", - id=>Workshop.CreateDescFile(id, outPath).GetAwaiter().GetResult(), - "mod_id", - 1) + new LaunchArgument(new []{"o", "out"}, + "sets output path", + p => outPath=p, + "out_path", + 0), + new LaunchArgument(new []{"clear"}, + "Clear mod files and put them into separate dirs in output dir. Requires -o", + wdir=>Workshop.ClearWorkshop(wdir, outPath), + "workshop_dir", + 1), + new LaunchArgument(new []{"diff"}, + "Compares mod files by hash", + p=>Diff.DiffCommandHandler(p), + "first_mod_directory:second_mod_directory:...", + 1), + new LaunchArgument(new []{"diff-detailed"}, + "reads conflicts_XXX.dtsod file and shows text diff for each file", + p=>Diff.DiffDetailedCommandHandler(p), + "conflicts_dtsod_path", + 1), + new LaunchArgument(new []{"merge-subdirs"}, + "Merges mods and shows conflicts. Requires -o", + d => Merge.MergeAll(Directory.GetDirectories(d), outPath), + "dir_with_mods", + 1), + new LaunchArgument(new []{"merge-into", "merge-single"}, + "Merges one mod into output dir and shows conflicts. Requires -o", + mod=>Merge.MergeInto(mod, outPath), + "mod_dir", + 1), + new LaunchArgument(new []{"gen-rus-locale"}, + "Creates l_russian copy of english locale in output directory. Requires -o", + eng=>Localisation.GenerateRussian(eng, outPath), + "english_locale_path", + 1), + new LaunchArgument(new []{"desc"}, + "Downloads mod description from steam to new file in outDir. Requires -o", + id=>Workshop.CreateDescFile(id, outPath).GetAwaiter().GetResult(), + "mod_id", + 1), + new LaunchArgument(new []{"rename"}, + "Renames mods in directory", + (modsdir, replace_pairs)=>Merge.RenameModsCommandHandler(modsdir, replace_pairs), + "dir_with_mods", "replace_pairs (old_name:new_name:...)", + 1), + new LaunchArgument(new []{"update-mods"}, + "Updates mods in [outdated_dir0...outdated_dirN] to new versions if found in updated_mods_dir. " + + "Moves old mods to backup_dir defined by -o.", + (updated, outdated)=>Merge.UpdateMods(updated, SplitArgToPaths(outdated, true), outPath), + "updated_mods_dir", "outdated_dir OR outdated_dir0:...:outdated_dirN", + 1), + new LaunchArgument(new []{"clean-locales"}, + "Deletes all localisations except l_russian and l_english.", + locdir=> Localisation.Clean(locdir), + "localisation_dir", + 1) ).ParseAndHandle(args); } catch (LaunchArgumentParser.ExitAfterHelpException) @@ -73,23 +90,27 @@ public static class Program catch (Exception ex) { Log("r", DTLib.Ben.Demystifier.ExceptionExtensions.ToStringDemystified(ex)); + Console.ResetColor(); + return 1; } - Console.ResetColor(); + + return 0; } - public static IOPath[] SplitStringToPaths(string connected_paths) + public static string[] SplitArgToStrings(string connected_parts, bool allow_one_part) { char part_sep; - if (connected_paths.Contains(':')) + if (connected_parts.Contains(':')) part_sep = ':'; - else if (!connected_paths.Contains(':')) + else if (connected_parts.Contains(';')) part_sep = ';'; - else throw new Exception($"<{connected_paths}> doesn't contain any separators (:/;)"); - - string[] split = connected_paths.Split(part_sep); - IOPath[] split_iop = new IOPath[split.Length]; - for (int i = 0; i < split.Length; i++) - split_iop[i] = new IOPath(split[i]); - return split_iop; + else if (allow_one_part) + return new []{connected_parts}; + else throw new Exception($"<{connected_parts}> doesn't contain any separators (:/;)"); + + return connected_parts.Split(part_sep); } + + public static IOPath[] SplitArgToPaths(string connected_parts, bool allow_one_part) => + IOPath.ArrayCast(SplitArgToStrings(connected_parts, allow_one_part)); } \ No newline at end of file