diff --git a/.gitignore b/.gitignore index ba5611fe8..d976c281f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ Debug/ DebugIntermediate/ ImperatorToCK3.UnitTests/obj/ ImperatorToCK3/obj/ +DocsGenerator/obj/ Publish/ diff --git a/DocsGenerator/.editorconfig b/DocsGenerator/.editorconfig new file mode 100644 index 000000000..ad1cabe94 --- /dev/null +++ b/DocsGenerator/.editorconfig @@ -0,0 +1,239 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = tab +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +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 + +# Indentation preferences +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 + +# Space preferences +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 + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# 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_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +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 +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 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 +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent \ No newline at end of file diff --git a/DocsGenerator/CulturesDocGenerator.cs b/DocsGenerator/CulturesDocGenerator.cs new file mode 100644 index 000000000..0426fb72d --- /dev/null +++ b/DocsGenerator/CulturesDocGenerator.cs @@ -0,0 +1,125 @@ +using commonItems; +using commonItems.Collections; +using commonItems.Colors; +using commonItems.Localization; +using commonItems.Mods; +using ImperatorToCK3.CK3.Cultures; + +namespace DocsGenerator; + +public static class CulturesDocGenerator { + private static IEnumerable LoadCultures(string modPath, ColorFactory colorFactory) { + // Create ModFilesystem, but with just the mod we're analyzing. + // For that reason, treat the mod as base game. + var modFS = new ModFilesystem(modPath, Array.Empty()); + + Logger.Info("Loading cultural pillars..."); + var pillars = new PillarCollection(); + pillars.LoadPillars(modFS); + + Logger.Info("Loading cultures..."); + var cultures = new CultureCollection(pillars); + cultures.LoadNameLists(modFS); + cultures.LoadCultures(modFS, colorFactory); + + return cultures; + } + + private static string GetLocForKey(LocDB locDB, string locKey) { + var locBlock = locDB.GetLocBlockForKey(locKey); + if (locBlock is null) { + Logger.Warn($"No localization found for \"{locKey}\""); + return locKey; + } + + var englishLoc = locBlock["english"]; + if (englishLoc is null) { + Logger.Warn($"No English localization found for \"{locKey}\""); + return locKey; + } + + // Check for nested loc. + var dollarPos = englishLoc.IndexOf('$'); + if (dollarPos != -1) { + var secondDollarPos = englishLoc.IndexOf('$', dollarPos + 1); + if (secondDollarPos != -1) { + var nesting = englishLoc.Substring(dollarPos, secondDollarPos - dollarPos + 1); + var nestedLocKey = nesting.Trim('$'); + englishLoc = englishLoc.Replace(nesting, GetLocForKey(locDB, nestedLocKey)); + } + } + return englishLoc; + } + + private static string GetCultureColorForCell(Culture culture) { + return "#" + culture.Color.OutputHex() + .Replace("hex", string.Empty) + .Replace("{", string.Empty) + .Replace("}", string.Empty) + .Trim(); + } + + private static void OutputCulturesTable(IEnumerable cultures, LocDB locDB, bool cultureColorUnderName) { + Logger.Info("Outputting cultures table..."); + using var output = new StringWriter(); + + output.WriteLine(""" + + """); + output.WriteLine(""); + output.WriteLine("\t"); + output.WriteLine("\t\t"); + output.WriteLine($""" + + + {(cultureColorUnderName ? "" : "")} + + + + + + + + + """); + output.WriteLine("\t\t\t"); + foreach (var culture in cultures) { + output.WriteLine("\t\t\t\t"); + if (cultureColorUnderName) { + output.WriteLine($"\t\t\t\t\t"); + } else { + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine($"\t\t\t\t\t"); + } + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine($"\t\t\t\t\t"); + output.WriteLine("\t\t\t\t"); + } + output.WriteLine("\t\t\t"); + output.WriteLine("\t\t
CultureHeritageEthosTraditionsLanguageMartial custom
{GetLocForKey(locDB, culture.Id)}{GetLocForKey(locDB, culture.Id)}{GetLocForKey(locDB, $"{culture.Heritage.Id}_name")}{GetLocForKey(locDB, $"{culture.EthosId}_name")}{string.Join("
", culture.Traditions.Select(t=>GetLocForKey(locDB, $"{t}_name")))}
{GetLocForKey(locDB, $"{culture.LanguageId}_name")}{GetLocForKey(locDB, $"{culture.MartialCustomId}_name")}
"); + output.WriteLine("\t"); + output.WriteLine(""); + + File.WriteAllText ("generated_docs/cultures_table.html", output.ToString()); + } + + public static void GenerateCulturesTable(string modPath, ColorFactory colorFactory, LocDB locDB, bool cultureColorUnderName) { + var cultures = LoadCultures(modPath, colorFactory); + OutputCulturesTable(cultures, locDB, cultureColorUnderName); + } +} \ No newline at end of file diff --git a/DocsGenerator/DocsGenerator.csproj b/DocsGenerator/DocsGenerator.csproj new file mode 100644 index 000000000..a7b20cd30 --- /dev/null +++ b/DocsGenerator/DocsGenerator.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + false + + + + + + + + + + + diff --git a/DocsGenerator/Options.cs b/DocsGenerator/Options.cs new file mode 100644 index 000000000..cecef7732 --- /dev/null +++ b/DocsGenerator/Options.cs @@ -0,0 +1,17 @@ +using CommandLine; + +namespace DocsGenerator; + +public class Options { + [Option('r', "gameRoot", Required = true, + HelpText = "CK3 game root path, for example \"C:/SteamLibrary/steamapps/common/Crusader Kings III/game\".")] + public string GameRoot { get; set; } = Directory.GetCurrentDirectory(); + + [Option('m', "modPath", Required = true, + HelpText = "Path to mod directory, for example \"C:/Users/User/Documents/Paradox Interactive/Crusader Kings III/mod/test_mod\"")] + public string ModPath { get; set; } = Directory.GetCurrentDirectory(); + + [Option('c', "cultureColorUnderName", Required = false, + HelpText = "Whether culture's color should be displayed under its name instead of inside a separate column.")] + public bool CultureColorUnderName { get; set; } = false; +} \ No newline at end of file diff --git a/DocsGenerator/Program.cs b/DocsGenerator/Program.cs new file mode 100644 index 000000000..e4d4c4af1 --- /dev/null +++ b/DocsGenerator/Program.cs @@ -0,0 +1,46 @@ +using CommandLine; +using commonItems; +using commonItems.Colors; +using commonItems.Localization; +using commonItems.Mods; +using DocsGenerator; +using ImperatorToCK3.CK3.Cultures; +using Parser = CommandLine.Parser; + +string gameRoot; +string modPath; + +Parser.Default.ParseArguments(args) + .WithParsed(o => { + gameRoot = o.GameRoot; + modPath = o.ModPath; + bool cultureColorUnderName = o.CultureColorUnderName; + + + if (!Directory.Exists(gameRoot)) { + Logger.Error($"\"{gameRoot}\" is not a directory."); + return; + } + if (!Directory.Exists(modPath)) { + Logger.Error($"\"{modPath}\" is not a directory."); + return; + } + + Logger.Info($"Generating docs for mod located in \"{modPath}\"..."); + Directory.CreateDirectory("generated_docs"); + + var mod = new Mod("analyzed mod", modPath); + var modFS = new ModFilesystem(gameRoot, new[] {mod}); + + var namedColors = new NamedColorCollection(); + namedColors.LoadNamedColors("common/named_colors", modFS); + var colorFactory = new ColorFactory(); + colorFactory.AddNamedColorDict(namedColors); + + var locDB = new LocDB("english"); + locDB.ScrapeLocalizations(modFS); + + CulturesDocGenerator.GenerateCulturesTable(modPath, colorFactory, locDB, cultureColorUnderName); + + Logger.Info("Finished generating mod docs."); + }); diff --git a/ImperatorToCK3.sln b/ImperatorToCK3.sln index 7d5791b47..24c24de9f 100644 --- a/ImperatorToCK3.sln +++ b/ImperatorToCK3.sln @@ -12,6 +12,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImperatorToCK3.UnitTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fronter", "Fronter.NET\Fronter.NET\Fronter.csproj", "{5AF479CA-A0FF-41D7-90E9-441B70EC84F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocsGenerator", "DocsGenerator\DocsGenerator.csproj", "{B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A6515254-8CB4-4354-90B1-865E35C3933B}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -49,6 +50,14 @@ Global {5AF479CA-A0FF-41D7-90E9-441B70EC84F2}.Release|Any CPU.Build.0 = Release|Any CPU {5AF479CA-A0FF-41D7-90E9-441B70EC84F2}.Release|x64.ActiveCfg = Release|Any CPU {5AF479CA-A0FF-41D7-90E9-441B70EC84F2}.Release|x64.Build.0 = Release|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Debug|x64.Build.0 = Debug|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Release|Any CPU.Build.0 = Release|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Release|x64.ActiveCfg = Release|Any CPU + {B6EB8AFB-760D-4BED-BF18-B1C230E1BF36}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ImperatorToCK3.sln.DotSettings b/ImperatorToCK3.sln.DotSettings index efa506d64..f6610f8e9 100644 --- a/ImperatorToCK3.sln.DotSettings +++ b/ImperatorToCK3.sln.DotSettings @@ -97,6 +97,7 @@ True True True + True True True True diff --git a/ImperatorToCK3/CK3/Cultures/Culture.cs b/ImperatorToCK3/CK3/Cultures/Culture.cs index b71f367d3..30703dbb6 100644 --- a/ImperatorToCK3/CK3/Cultures/Culture.cs +++ b/ImperatorToCK3/CK3/Cultures/Culture.cs @@ -2,6 +2,7 @@ using commonItems.Collections; using commonItems.Colors; using ImperatorToCK3.Exceptions; +using System.Collections.Generic; using System.Linq; namespace ImperatorToCK3.CK3.Cultures; @@ -11,6 +12,10 @@ public sealed class Culture : IIdentifiable { public Color Color { get; private set; } = new(0, 0, 0); public Pillar Heritage { get; private set; } public NameList NameList { get; private set; } + public string? LanguageId { get; private set; } + public string? EthosId { get; private set; } + public string? MartialCustomId { get; private set; } + public OrderedSet Traditions { get; } = new(); public Culture(string id, BufferedReader cultureReader, PillarCollection pillars, IdObjectCollection nameLists, ColorFactory colorFactory) { Id = id; @@ -21,11 +26,15 @@ public Culture(string id, BufferedReader cultureReader, PillarCollection pillars var heritageId = reader.GetString(); Heritage = pillars.Heritages.First(p => p.Id == heritageId); }); + parser.RegisterKeyword("ethos", reader => EthosId = reader.GetString()); + parser.RegisterKeyword("martial_custom", reader => MartialCustomId = reader.GetString()); + parser.RegisterKeyword("traditions", reader => Traditions.UnionWith(reader.GetStrings())); + parser.RegisterKeyword("language", reader => LanguageId = reader.GetString()); parser.RegisterKeyword("name_list", reader => { var nameListId = reader.GetString(); NameList = nameLists[nameListId]; }); - parser.IgnoreUnregisteredItems(); + parser.IgnoreAndStoreUnregisteredItems(IgnoredKeywords); parser.ParseStream(cultureReader); if (Heritage is null) { @@ -35,4 +44,6 @@ public Culture(string id, BufferedReader cultureReader, PillarCollection pillars throw new ConverterException($"Culture {id} has no name list defined!"); } } + + public static HashSet IgnoredKeywords { get; } = new(); } \ No newline at end of file diff --git a/ImperatorToCK3/CK3/World.cs b/ImperatorToCK3/CK3/World.cs index 6e769dda8..f19239862 100644 --- a/ImperatorToCK3/CK3/World.cs +++ b/ImperatorToCK3/CK3/World.cs @@ -12,6 +12,7 @@ using ImperatorToCK3.CK3.Titles; using ImperatorToCK3.Exceptions; using ImperatorToCK3.Imperator.Countries; +using ImperatorToCK3.Imperator.Cultures; using ImperatorToCK3.Imperator.Jobs; using ImperatorToCK3.Mappers.CoA; using ImperatorToCK3.Mappers.Culture; @@ -32,6 +33,7 @@ using System.Collections.Immutable; using System.IO; using System.Linq; +using Culture = ImperatorToCK3.CK3.Cultures.Culture; namespace ImperatorToCK3.CK3; diff --git a/ImperatorToCK3/Data_Files/blankMod/output/common/culture/cultures/IRToCK3_cultures_info.html b/ImperatorToCK3/Data_Files/blankMod/output/common/culture/cultures/IRToCK3_cultures_info.html deleted file mode 100644 index 7fcd9a3d8..000000000 --- a/ImperatorToCK3/Data_Files/blankMod/output/common/culture/cultures/IRToCK3_cultures_info.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HeritageCultureEthosTraditionsLanguageMartial customArchitectureFashionCoat of ArmsMilitary EquipmentOrigin
ArabicNabateanStoicCaravaneers; Dryland DwellersArabicMale OnlyMediterraneanAbbasidArabicArabicOriginal
ArabicAdnaniteStoicCaravaneers; Desert TravellersArabicMale OnlyArabicAbbasidArabicArabicOriginal
ArabicQahtaniteCommunalMountaineers; Maritime MercantilismArabicMale OnlyArabicAbbasidArabicArabicOriginal
BrythonicBrythonicStoicConcubines; Hill DwellersBrythonicMale OnlyContinental EuropeanContinental EuropeanBretonContinental EuropeanOriginal
- -