├── .editorconfig ├── .gitattributes ├── .gitignore ├── Debugging ├── DebugVars.cs └── ModDebug.cs ├── DemoSettingsClass.cs ├── ExtensionMethods ├── ExceptionExtensionMethods.cs ├── ICollectionExtensions.cs ├── IEnumerableExtensions.cs ├── MBBindingListExtensions.cs ├── SettingsBaseExtensionMethods.cs ├── StackExtension.cs └── StringExtensions.cs ├── FileDatabase ├── FileDatabase.cs ├── SettingType.cs └── SettingsDatabase.cs ├── GUI ├── Brushes │ ├── ButtonBrushes.xml │ ├── DividerBrushes.xml │ ├── ExpandIndicator.xml │ ├── ModSettingsItemBrush.xml │ ├── ResetButtonBrush.xml │ ├── SettingsValueDisplayBrush.xml │ └── TextBrushes.xml ├── GauntletUI │ ├── EditValueGauntletScreen.cs │ └── ModOptionsGauntletScreen.cs ├── ViewModels │ ├── EditValueVM.cs │ ├── ModSettingsScreenVM.cs │ ├── ModSettingsVM.cs │ ├── SettingProperty.cs │ └── SettingPropertyGroup.cs └── Views │ ├── EditValueTextWidget.cs │ ├── EditValueView.xml │ ├── HoverRichTextWidget.cs │ ├── ModLibSliderWidget.cs │ ├── ModOptionsScreen.xml │ ├── ModSettingsItem.xml │ ├── SettingPropertyGroupView.xml │ ├── SettingPropertyView.xml │ └── SettingsView.xml ├── GlobalSuppressions.cs ├── Interfaces ├── IAction.cs └── IInitial.cs ├── LICENSE ├── ModLib.Definitions ├── AssemblyChecker.cs ├── Attributes │ ├── SettingPropertyAttribute.cs │ └── SettingPropertyGroupAttribute.cs ├── Interfaces │ ├── ISerialisableFile.cs │ └── ISubFolder.cs ├── ModLib.Definitions.csproj ├── Properties │ └── AssemblyInfo.cs ├── SettingsBase.cs └── SettingsDatabase.cs ├── ModLib.Patches ├── GlobalSuppressions.cs ├── ModLib.Patches.csproj ├── ModLibPatchesSubModule.cs ├── Patches │ ├── DotNetPatch.cs │ └── ModulePatches.cs ├── Properties │ └── AssemblyInfo.cs ├── Settings.cs └── packages.config ├── ModLib.csproj ├── ModLib.props ├── ModLib.sln ├── ModLibSubModule.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── Ref.cs ├── SmeltingHelper.cs ├── SubModule.xml ├── UndoRedoStack ├── ComplexAction.cs ├── SetFloatSettingProperty.cs ├── SetIntSettingProperty.cs ├── SetValueAction.cs └── UndoRedoStack.cs └── packages.config /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,vb}] 2 | 3 | # IDE0060: Remove unused parameter 4 | dotnet_code_quality_unused_parameters = all:none 5 | 6 | # CA1303: Do not pass literals as localized parameters 7 | dotnet_diagnostic.CA1303.severity = none 8 | 9 | # CA1031: Do not catch general exception types 10 | dotnet_diagnostic.CA1031.severity = none 11 | 12 | # CA2227: Collection properties should be read only 13 | dotnet_diagnostic.CA2227.severity = none 14 | 15 | # CA1305: Specify IFormatProvider 16 | dotnet_diagnostic.CA1305.severity = none 17 | 18 | # CA1304: Specify CultureInfo 19 | dotnet_diagnostic.CA1304.severity = none 20 | 21 | #CA1801: 22 | dotnet_diagnostic.CA1801.severity = none 23 | 24 | #CA1822: 25 | dotnet_diagnostic.CA1822.severity = none 26 | 27 | # CA1307: Specify StringComparison 28 | dotnet_diagnostic.CA1307.severity = none 29 | 30 | # CA3075: Insecure DTD processing in XML 31 | dotnet_diagnostic.CA3075.severity = none 32 | 33 | # CA1720: Identifier contains type name 34 | dotnet_diagnostic.CA1720.severity = none 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /Debugging/DebugVars.cs: -------------------------------------------------------------------------------- 1 | namespace ModLib.Debugging 2 | { 3 | public static class DebugVars 4 | { 5 | public static bool ShowDebug { get; } = false; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Debugging/ModDebug.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Windows.Forms; 4 | 5 | namespace ModLib.Debugging 6 | { 7 | public static class ModDebug 8 | { 9 | public static void ShowError(string message, string title = "", Exception exception = null) 10 | { 11 | if (string.IsNullOrWhiteSpace(title)) 12 | title = ""; 13 | MessageBox.Show($"{message}\n\n{exception?.ToStringFull()}", title); 14 | } 15 | 16 | public static void ShowMessage(string message, string title = "", bool nonModal = false) 17 | { 18 | if (nonModal) 19 | { 20 | new Thread(() => MessageBox.Show(message, title)).Start(); 21 | } 22 | MessageBox.Show(message, title); 23 | } 24 | 25 | public static void LogError(string error, Exception ex = null) 26 | { 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DemoSettingsClass.cs: -------------------------------------------------------------------------------- 1 | using ModLib.Attributes; 2 | using System.Xml.Serialization; 3 | 4 | namespace MyMod 5 | { 6 | public class MyModSettings : SettingsBase 7 | { 8 | public const string InstanceID = "MyModSettings"; 9 | public override string ModName => "MyMod"; 10 | public override string ModuleFolderName => MyModFolder; 11 | [XmlElement] 12 | public override string ID { get; set; } = InstanceID; 13 | 14 | public static Settings Instance 15 | { 16 | get 17 | { 18 | return (MyModSettings)SettingsDatabase.GetSettings(InstanceID); 19 | } 20 | } 21 | 22 | [XmlElement] 23 | [SettingProperty("Some Multiplier", 1f, 3f, "This is a multiplier")] 24 | [SettingPropertyGroup("Group 1")] 25 | public float SomeMultiplier { get; set; } = 1.5f; 26 | [XmlElement] 27 | [SettingProperty("Some multiplier Enabled", "Enables SomeMultiplier")] 28 | [SettingPropertyGroup("Group 1")] 29 | public bool MultiplierEnabled { get; set; } = true; 30 | [XmlElement] 31 | [SettingProperty("Some Other Multiplier", 1f, 3f, "This is another multiplier")] 32 | [SettingPropertyGroup("Group 2")] 33 | public float SomeOtherMultiplier { get; set; } = 1.5f; 34 | } 35 | } -------------------------------------------------------------------------------- /ExtensionMethods/ExceptionExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ModLib 8 | { 9 | public static class ExceptionExtensionMethods 10 | { 11 | public static string ToStringFull(this Exception ex) 12 | { 13 | if (ex != null) 14 | return GetString(ex); 15 | else 16 | return ""; 17 | } 18 | 19 | private static string GetString(Exception ex) 20 | { 21 | StringBuilder sb = new StringBuilder(); 22 | GetStringRecursive(ex, sb); 23 | sb.AppendLine(); 24 | sb.AppendLine("Stack trace:"); 25 | sb.AppendLine(ex.StackTrace); 26 | return sb.ToString(); 27 | } 28 | 29 | private static void GetStringRecursive(Exception ex, StringBuilder sb) 30 | { 31 | sb.AppendLine($"{ex.GetType().Name}:"); 32 | sb.AppendLine(ex.Message); 33 | if (ex.InnerException != null) 34 | { 35 | sb.AppendLine(); 36 | GetStringRecursive(ex.InnerException, sb); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ExtensionMethods/ICollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using ModLib.GUI.ViewModels; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ModLib 6 | { 7 | public static class ICollectionExtensions 8 | { 9 | public static SettingPropertyGroup GetGroup(this ICollection groupsList, string groupName) 10 | { 11 | return groupsList.Where((x) => x.GroupName == groupName).FirstOrDefault(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ExtensionMethods/IEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ModLib 5 | { 6 | public static class IEnumerableExtensions 7 | { 8 | /// 9 | /// Applies the action to all elements in the collection. 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static IEnumerable Do(this IEnumerable enumerable, Action action) 15 | { 16 | if (enumerable != null) 17 | { 18 | foreach (var item in enumerable) 19 | action?.Invoke(item); 20 | return enumerable; 21 | } 22 | else 23 | return null; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ExtensionMethods/MBBindingListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using TaleWorlds.Library; 4 | 5 | namespace ModLib 6 | { 7 | public static class MBBindingListExtensions 8 | { 9 | public static void AddRange(this MBBindingList bindingList, List listToAdd) 10 | { 11 | if (listToAdd == null) throw new ArgumentNullException(nameof(listToAdd)); 12 | if (bindingList == null) throw new ArgumentNullException(nameof(bindingList)); 13 | if (listToAdd.Count == 1) 14 | bindingList.Add(listToAdd[0]); 15 | else if (listToAdd.Count > 0) 16 | { 17 | for (int i = listToAdd.Count - 1; i >= 0; i--) 18 | { 19 | bindingList.Add(listToAdd[i]); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ExtensionMethods/SettingsBaseExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using ModLib.Definitions; 2 | using ModLib.Definitions.Attributes; 3 | using ModLib.GUI.ViewModels; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace ModLib 10 | { 11 | public static class SettingsBaseExtensionMethods 12 | { 13 | private const char SubGroupDelimiter = '/'; 14 | 15 | internal static List GetSettingPropertyGroups(this SettingsBase sb) 16 | { 17 | var groups = new List(); 18 | // Find all the properties in the settings instance which have the SettingProperty attribute. 19 | var propList = (from p in sb.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) 20 | let propAttr = p.GetCustomAttribute(true) 21 | let groupAttr = p.GetCustomAttribute(true) 22 | where propAttr != null 23 | let groupAttrToAdd = groupAttr ?? SettingPropertyGroupAttribute.Default 24 | select new SettingProperty(propAttr, groupAttrToAdd, p, sb)).ToList(); 25 | 26 | //Loop through each property 27 | foreach (var settingProp in propList) 28 | { 29 | //First check that the setting property is set up properly. 30 | CheckIsValid(settingProp); 31 | //Find the group that the setting property should belong to. This is the default group if no group is specifically set with the SettingPropertyGroup attribute. 32 | SettingPropertyGroup group = GetGroupFor(settingProp, groups); 33 | group.Add(settingProp); 34 | } 35 | 36 | //If there is more than one group in the list, remove the misc group so that it can be added to the bottom of the list after sorting. 37 | SettingPropertyGroup miscGroup = GetGroupFor(SettingPropertyGroupAttribute.DefaultGroupName, groups); 38 | if (miscGroup != null && groups.Count > 1) 39 | groups.Remove(miscGroup); 40 | else 41 | miscGroup = null; 42 | 43 | //Sort the list of groups alphabetically. 44 | groups.Sort((x, y) => x.GroupName.CompareTo(y.GroupName)); 45 | if (miscGroup != null) 46 | groups.Add(miscGroup); 47 | 48 | foreach (var group in groups) 49 | group.SetParentGroup(null); 50 | 51 | return groups; 52 | } 53 | 54 | private static SettingPropertyGroup GetGroupFor(SettingProperty sp, ICollection groupsList) 55 | { 56 | //If the setting somehow doesn't have a group attribute, throw an error. 57 | if (sp.GroupAttribute == null) 58 | throw new Exception($"SettingProperty {sp.Name} has null GroupAttribute"); 59 | 60 | SettingPropertyGroup group; 61 | //Check if the intended group is a sub group 62 | if (sp.GroupAttribute.GroupName.Contains(SubGroupDelimiter)) 63 | { 64 | //Intended group is a sub group. Must find it. First get the top group. 65 | string topGroupName = GetTopGroupName(sp.GroupAttribute.GroupName, out string truncatedGroupName); 66 | SettingPropertyGroup topGroup = groupsList.GetGroup(topGroupName); 67 | if (topGroup == null) 68 | { 69 | topGroup = new SettingPropertyGroup(sp.GroupAttribute, topGroupName); 70 | groupsList.Add(topGroup); 71 | } 72 | //Find the sub group 73 | group = GetGroupForRecursive(truncatedGroupName, topGroup.SettingPropertyGroups, sp); 74 | } 75 | else 76 | { 77 | //Group is not a subgroup, can find it in the main list of groups. 78 | group = groupsList.GetGroup(sp.GroupAttribute.GroupName); 79 | if (group == null) 80 | { 81 | group = new SettingPropertyGroup(sp.GroupAttribute); 82 | groupsList.Add(group); 83 | } 84 | } 85 | return group; 86 | } 87 | 88 | private static SettingPropertyGroup GetGroupFor(string groupName, ICollection groupsList) 89 | { 90 | return groupsList.GetGroup(groupName); 91 | } 92 | 93 | private static SettingPropertyGroup GetGroupForRecursive(string groupName, ICollection groupsList, SettingProperty sp) 94 | { 95 | if (groupName.Contains(SubGroupDelimiter)) 96 | { 97 | //Need to go deeper 98 | string topGroupName = GetTopGroupName(groupName, out string truncatedGroupName); 99 | SettingPropertyGroup topGroup = GetGroupFor(topGroupName, groupsList); 100 | if (topGroup == null) 101 | { 102 | topGroup = new SettingPropertyGroup(sp.GroupAttribute, topGroupName); 103 | groupsList.Add(topGroup); 104 | } 105 | return GetGroupForRecursive(truncatedGroupName, topGroup.SettingPropertyGroups, sp); 106 | } 107 | else 108 | { 109 | //Reached the bottom level, can return the final group. 110 | SettingPropertyGroup group = groupsList.GetGroup(groupName); 111 | if (group == null) 112 | { 113 | group = new SettingPropertyGroup(sp.GroupAttribute, groupName); 114 | groupsList.Add(group); 115 | } 116 | return group; 117 | } 118 | } 119 | 120 | private static string GetTopGroupName(string groupName, out string truncatedGroupName) 121 | { 122 | int index = groupName.IndexOf(SubGroupDelimiter); 123 | string topGroupName = groupName.Substring(0, index); 124 | 125 | truncatedGroupName = groupName.Remove(0, index + 1); 126 | return topGroupName; 127 | } 128 | 129 | private static void CheckIsValid(SettingProperty prop) 130 | { 131 | if (!prop.Property.CanRead) 132 | throw new Exception($"Property {prop.Property.Name} in {prop.SettingsInstance.GetType().FullName} must have a getter."); 133 | if (!prop.Property.CanWrite) 134 | throw new Exception($"Property {prop.Property.Name} in {prop.SettingsInstance.GetType().FullName} must have a setter."); 135 | 136 | if (prop.SettingType == SettingType.Int || prop.SettingType == SettingType.Float) 137 | { 138 | if (prop.MinValue == prop.MaxValue) 139 | throw new Exception($"Property {prop.Property.Name} in {prop.SettingsInstance.GetType().FullName} is a numeric type but the MinValue and MaxValue are the same."); 140 | if (prop.GroupAttribute != null && prop.GroupAttribute.IsMainToggle) 141 | throw new Exception($"Property {prop.Property.Name} in {prop.SettingsInstance.GetType().FullName} is marked as the main toggle for the group but is a numeric type. The main toggle must be a boolean type."); 142 | } 143 | } 144 | 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ExtensionMethods/StackExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ModLib 6 | { 7 | public static class StackExtension 8 | { 9 | public static void AppendToTop(this Stack baseStack, Stack toAppend) 10 | { 11 | if (toAppend == null) throw new ArgumentNullException(nameof(toAppend)); 12 | if (baseStack == null) throw new ArgumentNullException(nameof(baseStack)); 13 | if (toAppend.Count == 0) 14 | return; 15 | 16 | T[] array = toAppend.ToArray(); 17 | for (int i = array.Length - 1; i >= 0; i--) 18 | { 19 | baseStack.Push(array[i]); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ExtensionMethods/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ModLib 2 | { 3 | public static class StringExtensions 4 | { 5 | public static int Count(this string str, char c) 6 | { 7 | int count = 0; 8 | if (!string.IsNullOrEmpty(str)) 9 | { 10 | foreach (var ch in str) 11 | { 12 | if (ch == c) count++; 13 | } 14 | } 15 | return count; 16 | } 17 | 18 | public static string Last(this string str) 19 | { 20 | if (!string.IsNullOrEmpty(str)) 21 | { 22 | if (str.Length > 0) 23 | { 24 | int index = str.Length - 1; 25 | return str[index].ToString(); 26 | } 27 | } 28 | 29 | return string.Empty; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FileDatabase/FileDatabase.cs: -------------------------------------------------------------------------------- 1 | using ModLib.Debugging; 2 | using ModLib.Definitions; 3 | using ModLib.Definitions.Interfaces; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Xml; 9 | using System.Xml.Serialization; 10 | 11 | namespace ModLib 12 | { 13 | public static class FileDatabase 14 | { 15 | private const string LoadablesFolderName = "Loadables"; 16 | public static Dictionary> Data { get; } = new Dictionary>(); 17 | 18 | /// 19 | /// Returns the ISerialisableFile of type T with the given ID from the database. If it cannot be found, returns null. 20 | /// 21 | /// Type of object to retrieve. 22 | /// ID of object to retrieve. 23 | /// Returns the instance of the object with the given type and ID. If it cannot be found, returns null. 24 | public static T Get(string id) where T : ISerialisableFile 25 | { 26 | //First check if the dictionary contains the key 27 | if (!Data.ContainsKey(typeof(T))) 28 | return default; 29 | if (!Data[typeof(T)].ContainsKey(id)) 30 | return default; 31 | 32 | return (T)Data[typeof(T)][id]; 33 | } 34 | 35 | /// 36 | /// Loads all files for the given module. 37 | /// 38 | /// Name of the module to load the files from. This is the name of the actual folder in the Bannerlord Modules folder. 39 | /// Returns true if initialisation was successful. 40 | public static bool Initialise(string moduleFolderName) 41 | { 42 | bool successful = false; 43 | try 44 | { 45 | LoadAllFiles(moduleFolderName); 46 | successful = true; 47 | } 48 | catch (Exception ex) 49 | { 50 | ModDebug.ShowError($"An error occurred whilst trying to load files for module: {moduleFolderName}", "Error occurred during loading files", ex); 51 | } 52 | return successful; 53 | } 54 | 55 | /// 56 | /// Saves the given object instance which inherits ISerialisableFile to an xml file. 57 | /// 58 | /// The folder name of the module to save to. 59 | /// Instance of the object to save to file. 60 | /// Indicates whether to save the file to the ModuleData/Loadables folder or to the mod's Config folder in Bannerlord's 'My Documents' directory. 61 | public static bool SaveToFile(string moduleFolderName, ISerialisableFile sf, Location location = Location.Modules) 62 | { 63 | try 64 | { 65 | if (sf == null) throw new ArgumentNullException(nameof(sf)); 66 | if (string.IsNullOrWhiteSpace(sf.ID)) 67 | throw new Exception($"FileDatabase tried to save an object of type {sf.GetType().FullName} but the ID value was null."); 68 | if (string.IsNullOrWhiteSpace(moduleFolderName)) 69 | throw new Exception($"FileDatabase tried to save an object of type {sf.GetType().FullName} with ID {sf.ID} but the module folder name given was null or empty."); 70 | 71 | //Gets the intended path for the file. 72 | string path = GetPathForModule(moduleFolderName, location); 73 | 74 | if (location == Location.Modules && !Directory.Exists(path)) 75 | throw new Exception($"FileDatabase cannot find the module named {moduleFolderName}"); 76 | else if (location == Location.Configs && !Directory.Exists(path)) 77 | Directory.CreateDirectory(path); 78 | 79 | if (location == Location.Modules) 80 | path = Path.Combine(path, "ModuleData", LoadablesFolderName); 81 | 82 | if (sf is ISubFolder) 83 | { 84 | ISubFolder subFolder = sf as ISubFolder; 85 | if (!string.IsNullOrWhiteSpace(subFolder.SubFolder)) 86 | path = Path.Combine(path, subFolder.SubFolder); 87 | } 88 | 89 | if (!Directory.Exists(path)) 90 | Directory.CreateDirectory(path); 91 | 92 | path = Path.Combine(path, GetFileNameFor(sf)); 93 | 94 | if (File.Exists(path)) 95 | File.Delete(path); 96 | 97 | using (XmlWriter writer = XmlWriter.Create(path, new XmlWriterSettings() { Indent = true, OmitXmlDeclaration = true })) 98 | { 99 | XmlRootAttribute rootNode = new XmlRootAttribute(); 100 | rootNode.ElementName = $"{sf.GetType().Assembly.GetName().Name}-{sf.GetType().FullName}"; 101 | XmlSerializerNamespaces xmlns = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }); 102 | var serializer = new XmlSerializer(sf.GetType(), rootNode); 103 | serializer.Serialize(writer, sf, xmlns); 104 | } 105 | return true; 106 | } 107 | catch (Exception ex) 108 | { 109 | ModDebug.ShowError($"Cannot create the file for type {sf?.GetType().FullName} with ID {sf?.ID} for module {moduleFolderName}:", "Error saving to file", ex); 110 | return false; 111 | } 112 | } 113 | 114 | /// 115 | /// Deletes the file for the given fileName and module. 116 | /// 117 | /// The folder name of the module to delete the file for. 118 | /// The file name of the file to be deleted. 119 | /// The location of the file to be deleted. 120 | /// Returns true if the file was deleted successfully. 121 | public static bool DeleteFile(string moduleFolderName, string fileName, Location location = Location.Modules) 122 | { 123 | bool successful = true; 124 | string path = GetPathForModule(moduleFolderName, location); 125 | if (!Directory.Exists(path)) 126 | { 127 | ModDebug.ShowError($"Tried to delete a file with file name {fileName} from directory \"{path}\" but the directory doesn't exist.", "Could not find directory"); 128 | successful = false; 129 | } 130 | 131 | if (location == Location.Modules) 132 | path = Path.Combine(path, "ModuleData", LoadablesFolderName); 133 | 134 | string filePath = Path.Combine(path, fileName); 135 | if (File.Exists(filePath)) 136 | File.Delete(filePath); 137 | 138 | return successful; 139 | } 140 | 141 | /// 142 | /// Deletes the file for the given object instance and module. 143 | /// 144 | /// The folder name of the module to delete the file for. 145 | /// The instance of the object whose file should be deleted. 146 | /// The location of the file to be deleted. 147 | /// Returns true if the file was deleted successfully. 148 | public static bool DeleteFile(string moduleFolderName, ISerialisableFile sf, Location location = Location.Modules) 149 | { 150 | return DeleteFile(moduleFolderName, GetFileNameFor(sf), location); 151 | } 152 | 153 | private static void Add(ISerialisableFile loadable) 154 | { 155 | if (loadable == null) 156 | throw new ArgumentNullException(nameof(loadable), "Tried to add something to the FileDatabase Data dictionary that was null"); 157 | if (string.IsNullOrWhiteSpace(loadable.ID)) 158 | throw new ArgumentNullException($"Loadable of type {loadable.GetType()} has missing ID field"); 159 | 160 | Type type = loadable.GetType(); 161 | //Special case for settings. We want them to all be saved under the SettingsBase key. 162 | if (type.IsSubclassOf(typeof(SettingsBase))) 163 | type = typeof(SettingsBase); 164 | 165 | if (!Data.ContainsKey(type)) 166 | Data.Add(type, new Dictionary()); 167 | 168 | if (Data[type].ContainsKey(loadable.ID)) 169 | { 170 | ModDebug.LogError($"Loader already contains Type: {type.AssemblyQualifiedName} ID: {loadable.ID}, overwriting..."); 171 | Data[type][loadable.ID] = loadable; 172 | } 173 | else 174 | Data[type].Add(loadable.ID, loadable); 175 | } 176 | 177 | internal static void LoadFromFile(string filePath) 178 | { 179 | using (XmlReader reader = XmlReader.Create(filePath)) 180 | { 181 | string nodeData = ""; 182 | try 183 | { 184 | //Find the type name 185 | if (reader.MoveToContent() == XmlNodeType.Element) 186 | nodeData = reader.Name; 187 | //If we couldn't find the type name, throw an exception saying so. If the root node doesn't include the namespace, throw an exception saying so. 188 | if (string.IsNullOrWhiteSpace(nodeData)) 189 | throw new Exception($"Could not find the root node in xml document located at {filePath}"); 190 | 191 | TypeData data = new TypeData(nodeData); 192 | //Find the type from the root node name. The root node should be the full name of the type, including the namespace and the assembly. 193 | 194 | if (data.Type == null) 195 | throw new Exception($"Unable to find type {data.FullName}"); 196 | 197 | XmlRootAttribute root = new XmlRootAttribute(); 198 | root.ElementName = nodeData; 199 | root.IsNullable = true; 200 | XmlSerializer serialiser = new XmlSerializer(data.Type, root); 201 | ISerialisableFile loaded = (ISerialisableFile)serialiser.Deserialize(reader); 202 | if (loaded != null) 203 | Add(loaded); 204 | else 205 | throw new Exception($"Unable to load {data.FullName} from file {filePath}."); 206 | } 207 | catch (Exception ex) 208 | { 209 | if (ex is ArgumentNullException && ((ArgumentNullException)ex).ParamName == "type") 210 | throw new Exception($"Cannot get a type from type name {nodeData} in file {filePath}", ex); 211 | throw new Exception($"An error occurred whilst loading file {filePath}", ex); 212 | } 213 | } 214 | } 215 | 216 | /// 217 | /// Loads all files in the Loadables folder for the given module and from the Documents folder for the given module. 218 | /// 219 | /// This is the name of the module to load the files for. This is the name of the module folder. 220 | private static void LoadAllFiles(string moduleName) 221 | { 222 | #region Loadables Folder 223 | //Check if the given module name is correct 224 | string modulePath = GetPathForModule(moduleName, Location.Modules); 225 | if (!Directory.Exists(modulePath)) 226 | throw new Exception($"Cannot find module named {moduleName}"); 227 | //Check the module's ModuleData folder for the Loadables folder. 228 | string moduleLoadablesPath = Path.Combine(modulePath, "ModuleData", LoadablesFolderName); 229 | if (Directory.Exists(moduleLoadablesPath)) 230 | { 231 | try 232 | { 233 | //If the module has a Loadables folder, loop through it and load all the files. 234 | //Starting with the files in the root folder 235 | foreach (var filePath in Directory.GetFiles(moduleLoadablesPath, "*.xml")) 236 | { 237 | LoadFromFile(filePath); 238 | } 239 | 240 | //Loop through any subfolders and load the files in them 241 | string[] subDirs = Directory.GetDirectories(moduleLoadablesPath); 242 | if (subDirs.Any()) 243 | { 244 | foreach (var subDir in subDirs) 245 | { 246 | foreach (var filePath in Directory.GetFiles(subDir, "*.xml")) 247 | { 248 | try 249 | { 250 | LoadFromFile(filePath); 251 | } 252 | catch (Exception ex) 253 | { 254 | ModDebug.LogError($"Failed to load file: {filePath} \n\nSkipping..\n\n", ex); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | catch (Exception ex) 261 | { 262 | throw new Exception($"An error occurred while FileDatabase was trying to load all files for module {moduleName}", ex); 263 | } 264 | } 265 | else 266 | Directory.CreateDirectory(moduleLoadablesPath); 267 | #endregion 268 | #region Documents Folder 269 | string modConfigsPath = GetPathForModule(moduleName, Location.Configs); 270 | if (Directory.Exists(modConfigsPath)) 271 | { 272 | foreach (string filePath in Directory.GetFiles(modConfigsPath)) 273 | { 274 | try 275 | { 276 | LoadFromFile(filePath); 277 | } 278 | catch (Exception ex) 279 | { 280 | ModDebug.LogError($"Failed to load file: {filePath}\n\n Skipping...", ex); 281 | } 282 | } 283 | string[] subfolders = Directory.GetDirectories(modConfigsPath); 284 | if (subfolders.Any()) 285 | { 286 | foreach (var subFolder in subfolders) 287 | { 288 | foreach (var filePath in Directory.GetFiles(subFolder)) 289 | { 290 | try 291 | { 292 | LoadFromFile(filePath); 293 | } 294 | catch (Exception ex) 295 | { 296 | ModDebug.LogError($"Failed to load file: {filePath}\n\n Skipping...", ex); 297 | } 298 | } 299 | } 300 | } 301 | } 302 | else 303 | Directory.CreateDirectory(modConfigsPath); 304 | #endregion 305 | } 306 | 307 | /// 308 | /// Returns the file name for the given ISerialisableFile 309 | /// 310 | /// The instance of the ISerialisableFile to retrieve the file name for. 311 | /// Returns the file name of the given ISerialisableFile, including the file extension. 312 | public static string GetFileNameFor(ISerialisableFile sf) 313 | { 314 | if (sf == null) throw new ArgumentNullException(nameof(sf)); 315 | return $"{sf.GetType().Name}.{sf.ID}.xml"; 316 | } 317 | 318 | /// 319 | /// Returns the root folder for the given module name and intended location. 320 | /// 321 | /// Name of the Module's Folder. 322 | /// Which location to get the path to - configs or the mod's module folder. 323 | /// 324 | public static string GetPathForModule(string moduleFolderName, Location location) 325 | { 326 | if (location == Location.Modules) 327 | return Path.Combine(TaleWorlds.Library.BasePath.Name, "Modules", moduleFolderName); 328 | else 329 | return Path.Combine(TaleWorlds.Engine.Utilities.GetConfigsPath(), moduleFolderName); 330 | } 331 | 332 | private class TypeData 333 | { 334 | public string AssemblyName { get; private set; } = ""; 335 | public string TypeName { get; private set; } = ""; 336 | public string FullName => $"{TypeName}, {AssemblyName}"; 337 | private Type _type = null; 338 | public Type Type 339 | { 340 | get 341 | { 342 | if (_type == null) 343 | _type = AppDomain.CurrentDomain.GetAssemblies().Where(z => z.FullName.StartsWith(AssemblyName)).FirstOrDefault().GetType(TypeName); 344 | return _type; 345 | } 346 | } 347 | 348 | public TypeData(string nodeData) 349 | { 350 | if (!string.IsNullOrWhiteSpace(nodeData)) 351 | { 352 | if (!nodeData.Contains("-")) 353 | throw new ArgumentException($"Node data does not contain an assembly string\nNode Data: {nodeData}"); 354 | if (!nodeData.Contains(".")) 355 | throw new ArgumentException($"Node data does not contain a namespace string\nNode Data: {nodeData}"); 356 | 357 | string[] split = nodeData.Split('-'); 358 | 359 | if (!string.IsNullOrWhiteSpace(split[0])) 360 | AssemblyName = split[0]; 361 | else 362 | throw new ArgumentException($"Assembly name in node data was null or empty\nNode Data: {nodeData}"); 363 | 364 | if (!string.IsNullOrWhiteSpace(split[1])) 365 | TypeName = split[1]; 366 | else 367 | throw new ArgumentException($"Type name in node data was null or empty\nNode Data: {nodeData}"); 368 | } 369 | else 370 | throw new ArgumentException($"The given node data was invalid.\nNode Data: {nodeData}"); 371 | } 372 | } 373 | 374 | public enum Location 375 | { 376 | Modules, 377 | Configs 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /FileDatabase/SettingType.cs: -------------------------------------------------------------------------------- 1 | namespace ModLib 2 | { 3 | public enum SettingType 4 | { 5 | Bool, 6 | Int, 7 | Float 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /FileDatabase/SettingsDatabase.cs: -------------------------------------------------------------------------------- 1 | using ModLib.Debugging; 2 | using ModLib.Definitions; 3 | using ModLib.GUI.ViewModels; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | 9 | namespace ModLib 10 | { 11 | internal static class SettingsDatabase 12 | { 13 | private static List _modSettingsVMs = null; 14 | private static Dictionary AllSettingsDict { get; } = new Dictionary(); 15 | 16 | public static List AllSettings => AllSettingsDict.Values.ToList(); 17 | public static int SettingsCount => AllSettingsDict.Values.Count; 18 | public static List ModSettingsVMs 19 | { 20 | get 21 | { 22 | if (_modSettingsVMs == null) 23 | { 24 | BuildModSettingsVMs(); 25 | } 26 | return _modSettingsVMs; 27 | } 28 | } 29 | 30 | /// 31 | /// Registers the settings class with the SettingsDatabase for use in the settings menu. 32 | /// 33 | /// Intance of the settings object to be registered with the SettingsDatabase. 34 | /// Returns true if successful. Returns false if the object's ID key is already present in the SettingsDatabase. 35 | private static bool RegisterSettings(SettingsBase settings) 36 | { 37 | if (settings == null) throw new ArgumentNullException(nameof(settings)); 38 | if (!AllSettingsDict.ContainsKey(settings.ID)) 39 | { 40 | AllSettingsDict.Add(settings.ID, settings); 41 | _modSettingsVMs = null; 42 | return true; 43 | } 44 | else 45 | { 46 | //TODO:: When debugging log is finished, show a message saying that the key already exists 47 | return false; 48 | } 49 | } 50 | 51 | /// 52 | /// Retrieves the Settings instance from the SettingsDatabase with the given ID. 53 | /// 54 | /// The ID for the settings instance. 55 | /// Returns the settings instance with the given ID. Returns null if nothing can be found. 56 | internal static SettingsBase GetSettings(string uniqueID) 57 | { 58 | if (AllSettingsDict.ContainsKey(uniqueID)) 59 | { 60 | return AllSettingsDict[uniqueID]; 61 | } 62 | else 63 | return null; 64 | } 65 | 66 | /// 67 | /// Saves the settings instance to file. 68 | /// 69 | /// Instance of the settings object to save to file. 70 | /// Return true if the settings object was saved successfully. Returns false if it failed to save. 71 | internal static bool SaveSettings(SettingsBase settingsInstance) 72 | { 73 | if (settingsInstance == null) throw new ArgumentNullException(nameof(settingsInstance)); 74 | return FileDatabase.SaveToFile(settingsInstance.ModuleFolderName, settingsInstance, FileDatabase.Location.Configs); 75 | } 76 | 77 | /// 78 | /// Resets the settings instance to the default values for that instance. 79 | /// 80 | /// The instance of the object to be reset 81 | /// Returns the instance of the new object with default values. 82 | internal static SettingsBase ResetSettingsInstance(SettingsBase settingsInstance) 83 | { 84 | if (settingsInstance == null) throw new ArgumentNullException(nameof(settingsInstance)); 85 | string id = settingsInstance.ID; 86 | SettingsBase newObj = (SettingsBase)Activator.CreateInstance(settingsInstance.GetType()); 87 | newObj.ID = id; 88 | AllSettingsDict[id] = newObj; 89 | return newObj; 90 | } 91 | 92 | internal static bool OverrideSettingsWithID(SettingsBase settings, string ID) 93 | { 94 | if (AllSettingsDict.ContainsKey(ID)) 95 | { 96 | AllSettingsDict[ID] = settings; 97 | return true; 98 | } 99 | return false; 100 | } 101 | 102 | internal static void LoadAllSettings() 103 | { 104 | List types = new List(); 105 | 106 | foreach (var assem in AppDomain.CurrentDomain.GetAssemblies()) 107 | { 108 | var list = (from t in assem.GetTypes() 109 | where t != typeof(SettingsBase) && t.IsSubclassOf(typeof(SettingsBase)) && !t.IsAbstract && !t.IsInterface 110 | select t).ToList(); 111 | 112 | if (list.Any()) 113 | types.AddRange(list); 114 | } 115 | 116 | if (types.Any()) 117 | { 118 | foreach (var t in types) 119 | { 120 | LoadSettingsFromType(t); 121 | } 122 | } 123 | } 124 | 125 | internal static void LoadSettingsFromType(Type t) 126 | { 127 | SettingsBase defaultSB = (SettingsBase)Activator.CreateInstance(t); 128 | SettingsBase sb = FileDatabase.Get(defaultSB.ID); 129 | if (sb == null) 130 | { 131 | string path = Path.Combine(FileDatabase.GetPathForModule(defaultSB.ModuleFolderName, FileDatabase.Location.Configs), FileDatabase.GetFileNameFor(defaultSB)); 132 | if (File.Exists(path)) 133 | { 134 | FileDatabase.LoadFromFile(path); 135 | sb = FileDatabase.Get(defaultSB.ID); 136 | } 137 | if (sb == null) 138 | sb = defaultSB; 139 | } 140 | RegisterSettings(sb); 141 | } 142 | 143 | internal static void BuildModSettingsVMs() 144 | { 145 | try 146 | { 147 | _modSettingsVMs = new List(); 148 | foreach (var settings in AllSettings) 149 | { 150 | ModSettingsVM msvm = new ModSettingsVM(settings); 151 | _modSettingsVMs.Add(msvm); 152 | } 153 | _modSettingsVMs.Sort((x, y) => y.ModName.CompareTo(x.ModName)); 154 | } 155 | catch (Exception ex) 156 | { 157 | ModDebug.ShowError("An error occurred while creating the ViewModels for all mod settings", "Error Occurred", ex); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /GUI/Brushes/ButtonBrushes.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /GUI/Brushes/DividerBrushes.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /GUI/Brushes/ExpandIndicator.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /GUI/Brushes/ModSettingsItemBrush.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 10 | 13 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /GUI/Brushes/ResetButtonBrush.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 10 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /GUI/Brushes/SettingsValueDisplayBrush.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /GUI/Brushes/TextBrushes.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |