diff --git a/00_Utilities/DotnetUtils/.editorconfig b/00_Utilities/DotnetUtils/.editorconfig
new file mode 100644
index 00000000..9e7a90d5
--- /dev/null
+++ b/00_Utilities/DotnetUtils/.editorconfig
@@ -0,0 +1,168 @@
+root = true
+
+
+[*.{cs,vb}]
+indent_size = 4
+indent_style = space
+end_of_line = crlf
+insert_final_newline = true
+
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent
+
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = end_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_private_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_members_should_be_pascal_case.symbols = non_private_members
+dotnet_naming_rule.non_private_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.private_members_should_be_pascal_case.symbols = private_members
+dotnet_naming_rule.private_members_should_be_pascal_case.style = camel_case
+
+
+# Symbols for use with naming rules
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_private_members.applicable_kinds = property, method, field, event
+dotnet_naming_symbols.non_private_members.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+
+dotnet_naming_symbols.private_members.applicable_kinds = property, method, field, event
+dotnet_naming_symbols.private_members.applicable_accessibilities = private
+
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.camel_case.required_prefix =
+dotnet_naming_style.camel_case.required_suffix =
+dotnet_naming_style.camel_case.word_separator =
+dotnet_naming_style.camel_case.capitalization = camel_case
+
+
+[*.cs]
+csharp_new_line_before_catch = false
+csharp_new_line_before_else = false
+csharp_new_line_before_finally = false
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = none
+csharp_new_line_between_query_expression_clauses = true
+
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+csharp_prefer_braces = true:warning
+
+csharp_style_expression_bodied_constructors = true:suggestion
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+
+csharp_prefer_simple_default_expression = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+csharp_preferred_modifier_order = internal,protected,public,private,static,readonly,abstract,override,sealed,virtual:suggestion
+
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+
+[*.vb]
+visual_basic_preferred_modifier_order = partial,default,private,protected,public,friend,notoverridable,overridable,mustoverride,overloads,overrides,mustinherit,notinheritable,static,shared,shadows,readonly,writeonly,dim,const,withevents,widening,narrowing,custom,async,iterator:silent
+visual_basic_style_unused_value_assignment_preference = unused_local_variable:suggestion
+visual_basic_style_unused_value_expression_statement_preference = unused_local_variable:silent
diff --git a/00_Utilities/DotnetUtils/DotnetUtils.sln b/00_Utilities/DotnetUtils/DotnetUtils.sln
new file mode 100644
index 00000000..ecf9588c
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetUtils", "DotnetUtils\DotnetUtils.csproj", "{BFDF93C2-4FB7-4838-AFDF-E7B5F83C3F00}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BFDF93C2-4FB7-4838-AFDF-E7B5F83C3F00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFDF93C2-4FB7-4838-AFDF-E7B5F83C3F00}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFDF93C2-4FB7-4838-AFDF-E7B5F83C3F00}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFDF93C2-4FB7-4838-AFDF-E7B5F83C3F00}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {30FCF56E-4E83-42F8-AB43-A52C86C7C9B4}
+ EndGlobalSection
+EndGlobal
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/DotnetUtils.csproj b/00_Utilities/DotnetUtils/DotnetUtils/DotnetUtils.csproj
new file mode 100644
index 00000000..74abf5c9
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/DotnetUtils.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/Extensions.cs b/00_Utilities/DotnetUtils/DotnetUtils/Extensions.cs
new file mode 100644
index 00000000..82ff9532
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/Extensions.cs
@@ -0,0 +1,27 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace DotnetUtils;
+
+public static class Extensions {
+ public static IEnumerable SelectT(this IEnumerable<(T1, T2)> src, Func selector) =>
+ src.Select(x => selector(x.Item1, x.Item2));
+ public static IEnumerable SelectT(this IEnumerable<(T1, T2, T3)> src, Func selector) =>
+ src.Select(x => selector(x.Item1, x.Item2, x.Item3));
+ public static IEnumerable<(T1, T2, int)> WithIndex(this IEnumerable<(T1, T2)> src) => src.Select((x, index) => (x.Item1, x.Item2, index));
+
+ public static bool IsNullOrWhitespace([NotNullWhen(false)] this string? s) => string.IsNullOrWhiteSpace(s);
+
+ [return: NotNullIfNotNull("path")]
+ public static string? RelativePath(this string? path, string? rootPath) {
+ if (
+ path.IsNullOrWhitespace() ||
+ rootPath.IsNullOrWhitespace()
+ ) { return path; }
+
+ var path1 = path.TrimEnd('\\');
+ rootPath = rootPath.TrimEnd('\\');
+ if (!path1.StartsWith(rootPath, StringComparison.InvariantCultureIgnoreCase)) { return path; }
+
+ return path1[(rootPath.Length + 1)..]; // ignore the initial /
+ }
+}
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/Globals.cs b/00_Utilities/DotnetUtils/DotnetUtils/Globals.cs
new file mode 100644
index 00000000..1ff7c09d
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/Globals.cs
@@ -0,0 +1,8 @@
+namespace DotnetUtils;
+
+public static class Globals {
+ public static readonly Dictionary LangData = new() {
+ { "csharp", ("cs", "csproj") },
+ { "vbnet", ("vb", "vbproj") }
+ };
+}
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/PortInfo.cs b/00_Utilities/DotnetUtils/DotnetUtils/PortInfo.cs
new file mode 100644
index 00000000..894c8834
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/PortInfo.cs
@@ -0,0 +1,51 @@
+using System.Reflection;
+using static System.IO.Directory;
+using static System.IO.Path;
+using static DotnetUtils.Globals;
+
+namespace DotnetUtils;
+
+public record PortInfo(
+ string FullPath, string FolderName, int Index, string GameName,
+ string LangPath, string Lang, string Ext, string ProjExt,
+ string[] CodeFiles, string[] Slns, string[] Projs
+) {
+
+ private static readonly EnumerationOptions enumerationOptions = new() {
+ RecurseSubdirectories = true,
+ MatchType = MatchType.Simple,
+ MatchCasing = MatchCasing.CaseInsensitive
+ };
+
+ public static PortInfo? Create(string fullPath, string langKeyword) {
+ var folderName = GetFileName(fullPath);
+ var parts = folderName.Split('_', 2);
+
+ var index =
+ parts.Length > 0 && int.TryParse(parts[0], out var n) ?
+ n :
+ (int?)null;
+
+ var gameName =
+ parts.Length > 1 ?
+ parts[1].Replace("_", "") :
+ null;
+
+ if (index is 0 or null || gameName is null) { return null; }
+
+ var (ext, projExt) = LangData[langKeyword];
+ var langPath = Combine(fullPath, langKeyword);
+ var codeFiles =
+ GetFiles(langPath, $"*.{ext}", enumerationOptions)
+ .Where(x => !x.Contains("\\bin\\") && !x.Contains("\\obj\\"))
+ .ToArray();
+
+ return new PortInfo(
+ fullPath, folderName, index.Value, gameName,
+ langPath, langKeyword, ext, projExt,
+ codeFiles,
+ GetFiles(langPath, "*.sln", enumerationOptions),
+ GetFiles(langPath, $"*.{projExt}", enumerationOptions)
+ );
+ }
+}
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/PortInfos.cs b/00_Utilities/DotnetUtils/DotnetUtils/PortInfos.cs
new file mode 100644
index 00000000..b8c1afd3
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/PortInfos.cs
@@ -0,0 +1,22 @@
+using System.Reflection;
+using static System.IO.Directory;
+using static DotnetUtils.Globals;
+
+namespace DotnetUtils;
+
+public static class PortInfos {
+ public static readonly string Root;
+
+ static PortInfos() {
+ Root = GetParent(Assembly.GetEntryAssembly()!.Location)!.FullName;
+ Root = Root[..Root.IndexOf(@"\00_Utilities")];
+
+ Get = GetDirectories(Root)
+ .SelectMany(fullPath => LangData.Keys.Select(keyword => (fullPath, keyword)))
+ .SelectT((fullPath, keyword) => PortInfo.Create(fullPath, keyword))
+ .Where(x => x is not null)
+ .ToArray()!;
+ }
+
+ public static readonly PortInfo[] Get;
+}
diff --git a/00_Utilities/DotnetUtils/DotnetUtils/Program.cs b/00_Utilities/DotnetUtils/DotnetUtils/Program.cs
new file mode 100644
index 00000000..9bf36f4f
--- /dev/null
+++ b/00_Utilities/DotnetUtils/DotnetUtils/Program.cs
@@ -0,0 +1,163 @@
+using DotnetUtils;
+using static System.Console;
+using static System.IO.Path;
+
+var infos = PortInfos.Get;
+
+var actions = new (Action action, string description)[] {
+ (printInfos, "Output information -- solution, project, and code files"),
+ (missingSln, "Output missing sln"),
+ (unexpectedSlnName, "Output misnamed sln"),
+ (multipleSlns, "Output multiple sln files"),
+ (missingProj, "Output missing project file"),
+ (unexpectedProjName, "Output misnamed project files"),
+ (multipleProjs, "Output multiple project files")
+};
+
+foreach (var (_, description, index) in actions.WithIndex()) {
+ WriteLine($"{index}: {description}");
+}
+
+WriteLine();
+
+actions[getChoice(actions.Length - 1)].action();
+
+int getChoice(int maxValue) {
+ int result;
+ do {
+ Write("? ");
+ } while (!int.TryParse(ReadLine(), out result) || result < 0 || result > maxValue);
+ WriteLine();
+ return result;
+}
+
+void printSlns(PortInfo pi) {
+ switch (pi.Slns.Length) {
+ case 0:
+ WriteLine("No sln");
+ break;
+ case 1:
+ WriteLine($"Solution: {pi.Slns[0].RelativePath(pi.LangPath)}");
+ break;
+ case > 1:
+ WriteLine("Solutions:");
+ foreach (var sln in pi.Slns) {
+ Write(sln.RelativePath(pi.LangPath));
+ WriteLine();
+ }
+ break;
+ }
+}
+
+void printProjs(PortInfo pi) {
+ switch (pi.Projs.Length) {
+ case 0:
+ WriteLine("No project");
+ break;
+ case 1:
+ WriteLine($"Project: {pi.Projs[0].RelativePath(pi.LangPath)}");
+ break;
+ case > 1:
+ WriteLine("Projects:");
+ foreach (var proj in pi.Projs) {
+ Write(proj.RelativePath(pi.LangPath));
+ WriteLine();
+ }
+ break;
+ }
+ WriteLine();
+}
+
+void printInfos() {
+ foreach (var item in infos) {
+ WriteLine(item.LangPath);
+ WriteLine();
+
+ printSlns(item);
+ WriteLine();
+
+ printProjs(item);
+ WriteLine();
+
+ // get code files
+ foreach (var file in item.CodeFiles) {
+ WriteLine(file.RelativePath(item.LangPath));
+ }
+ WriteLine(new string('-', 50));
+ }
+}
+
+void missingSln() {
+ var data = infos.Where(x => !x.Slns.Any()).ToArray();
+ foreach (var item in data) {
+ WriteLine(item.LangPath);
+ }
+ WriteLine();
+ WriteLine($"Count: {data.Length}");
+}
+
+void unexpectedSlnName() {
+ var counter = 0;
+ foreach (var item in infos) {
+ if (!item.Slns.Any()) { continue; }
+
+ var expectedSlnName = $"{item.GameName}.sln";
+ if (item.Slns.Contains(Combine(item.LangPath, expectedSlnName))) { continue; }
+
+ counter += 1;
+ WriteLine(item.LangPath);
+ WriteLine($"Expected: {expectedSlnName}");
+
+ printSlns(item);
+
+ WriteLine();
+ }
+ WriteLine($"Count: {counter}");
+}
+
+void multipleSlns() {
+ var data = infos.Where(x => x.Slns.Length > 1).ToArray();
+ foreach (var item in data) {
+ WriteLine(item.LangPath);
+ printSlns(item);
+ }
+ WriteLine();
+ WriteLine($"Count: {data.Length}");
+}
+
+void missingProj() {
+ var data = infos.Where(x => !x.Projs.Any()).ToArray();
+ foreach (var item in data) {
+ WriteLine(item.LangPath);
+ }
+ WriteLine();
+ WriteLine($"Count: {data.Length}");
+}
+
+void unexpectedProjName() {
+ var counter = 0;
+ foreach (var item in infos) {
+ if (!item.Projs.Any()) { continue; }
+
+ var expectedProjName = $"{item.GameName}.{item.ProjExt}";
+ if (item.Projs.Contains(Combine(item.LangPath, expectedProjName))) { continue; }
+
+ counter += 1;
+ WriteLine(item.LangPath);
+ WriteLine($"Expected: {expectedProjName}");
+
+ printProjs(item);
+
+ WriteLine();
+ }
+ WriteLine($"Count: {counter}");
+}
+
+void multipleProjs() {
+ var data = infos.Where(x => x.Projs.Length > 1).ToArray();
+ foreach (var item in data) {
+ WriteLine(item.LangPath);
+ }
+ WriteLine();
+ WriteLine($"Count: {data.Length}");
+}