├── .github └── workflows │ └── csframework.yml ├── .gitignore ├── LICENSE ├── README.md ├── UE4Config.Tests ├── Evaluation │ └── PropertyEvaluatorTests.cs ├── Hierarchy │ ├── ConfigBranchExtensionsTests.cs │ ├── ConfigFileProviderTests.cs │ ├── ConfigFileReferenceTests.cs │ ├── ConfigHierarchyLevelRangeTests.cs │ ├── ConfigHierarchyLevelTests.cs │ ├── ConfigHierarchyTests.cs │ ├── DataDrivenPlatformProviderTests.cs │ ├── FileConfigHierarchyTests.cs │ ├── IConfigFileProviderTests.cs │ ├── IConfigPlatformExtensionsTests.cs │ ├── IConfigTreeExtensionsTests.cs │ ├── IConfigTreeTests.cs │ ├── VirtualConfigCacheTests.cs │ ├── VirtualConfigTreeTests.cs │ └── VirtualConfigsCacheTests.cs ├── Parsing │ ├── ConfigIniSectionTests.cs │ ├── ConfigIniTests.cs │ ├── ConfigIniWriterTests.cs │ ├── InstructionTokenTests.cs │ ├── InstructionTypeTests.cs │ ├── LineEndingTests.cs │ ├── LineTokenTests.cs │ ├── MultilineTokenTests.cs │ └── WhitespaceTokenTests.cs ├── Properties │ └── AssemblyInfo.cs ├── SampleTests.cs ├── SanityTests.cs ├── TestData │ ├── MockEngine │ │ ├── Config │ │ │ ├── Base.ini │ │ │ ├── BaseGame.ini │ │ │ ├── Switch │ │ │ │ └── SwitchGame.ini │ │ │ └── Windows │ │ │ │ └── WindowsGame.ini │ │ └── Platforms │ │ │ ├── Linux │ │ │ └── Config │ │ │ │ └── LinuxGame.ini │ │ │ ├── Switch │ │ │ └── Config │ │ │ │ └── SwitchGame.ini │ │ │ └── XBoxOne │ │ │ └── Config │ │ │ └── XBoxOneGame.ini │ └── MockProject │ │ ├── Config │ │ ├── DefaultEditor.ini │ │ ├── DefaultEngine.ini │ │ ├── DefaultGame.ini │ │ ├── Switch │ │ │ └── SwitchGame.ini │ │ └── Windows │ │ │ └── WindowsGame.ini │ │ └── Platforms │ │ ├── Mac │ │ └── Config │ │ │ └── MacGame.ini │ │ ├── Switch │ │ └── Config │ │ │ └── SwitchGame.ini │ │ └── XBoxOne │ │ └── Config │ │ └── XBoxOneGame.ini ├── TestUtils.cs ├── UE4Config.Tests.csproj └── packages.config ├── UE4Config.sln ├── UE4Config ├── Evaluation │ └── PropertyEvaluator.cs ├── Hierarchy │ ├── ConfigBranchExtensions.cs │ ├── ConfigCategory.cs │ ├── ConfigDomain.cs │ ├── ConfigFileIOAdapter.cs │ ├── ConfigFileProvider.cs │ ├── ConfigFileReference.cs │ ├── ConfigHierarchy.cs │ ├── ConfigHierarchyLevel.cs │ ├── ConfigHierarchyLevelRange.cs │ ├── ConfigPlatform.cs │ ├── ConfigReferenceTree427.cs │ ├── DataDrivenPlatformInfo.cs │ ├── DataDrivenPlatformProvider.cs │ ├── FileConfigHierarchy.cs │ ├── IConfigFileIOAdapter.cs │ ├── IConfigFileProvider.cs │ ├── IConfigFileProviderAutoPlatformModel.cs │ ├── IConfigPlatform.cs │ ├── IConfigReferenceTree.cs │ ├── VirtualConfigCache.cs │ ├── VirtualConfigTree.cs │ ├── VirtualConfigTreeUtility.cs │ └── VirtualConfigsCache.cs ├── Parsing │ ├── CommentToken.cs │ ├── ConfigIni.cs │ ├── ConfigIniSection.cs │ ├── ConfigIniWriter.cs │ ├── IniToken.cs │ ├── InstructionToken.cs │ ├── InstructionType.cs │ ├── LineEnding.cs │ ├── LineToken.cs │ ├── MultilineToken.cs │ ├── TextLine.cs │ ├── TextToken.cs │ └── WhitespaceToken.cs └── UE4Config.csproj └── appveyor.yml /.github/workflows/csframework.yml: -------------------------------------------------------------------------------- 1 | name: .net Framework 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | name: Checkout Code 13 | 14 | - name: Setup MSBuild Path 15 | uses: microsoft/setup-msbuild@v1.1 16 | 17 | - name: Restore NuGet Packages 18 | run: nuget restore 19 | 20 | - name: Install NUnit.ConsoleRunner 21 | run: nuget install NUnit.ConsoleRunner -Version 3.13.0 -DirectDownload -OutputDirectory . 22 | 23 | - name: Build Tests 24 | run: msbuild UE4Config.Tests/UE4Config.Tests.csproj /p:Configuration=Debug 25 | 26 | - name: Run Tests 27 | run: ./NUnit.ConsoleRunner.3.13.0/tools/nunit3-console.exe UE4Config.Tests/bin/Debug/UE4Config.Tests.dll /xml=nunit-result.xml 28 | 29 | - name: Upload test results 30 | uses: actions/upload-artifact@v3 31 | with: 32 | name: nunit-result.xml 33 | path: nunit-result.xml 34 | # Use always() to always run this step to publish test results when there are test failures 35 | if: ${{ always() }} 36 | -------------------------------------------------------------------------------- /.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 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patrick Michael Hopf a.k.a. Worry Darque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UE4Config 2 | 3 | A straightlaced C# libary to evaluate & edit Unreal Engine 4 config files, for UE4 projects and built games. 4 | [![GitHub release](https://img.shields.io/github/release/Wortex17/UE4Config)](https://github.com/Wortex17/UE4Config/releases/latest) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/f5tq5q3u4j87a0ux/branch/master?svg=true)](https://ci.appveyor.com/project/Wortex17/UE4Config/branch/master) 6 | [![Nuget](https://img.shields.io/nuget/v/Infrablack.UE4Config)](https://www.nuget.org/packages/Infrablack.UE4Config) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f679eceb343c47d581494ad6b6b9f809)](https://app.codacy.com/manual/Wortex17/UE4Config?utm_source=github.com&utm_medium=referral&utm_content=Wortex17/UE4Config&utm_campaign=Badge_Grade_Dashboard) 8 | [![codecov](https://codecov.io/gh/Wortex17/UE4Config/branch/master/graph/badge.svg)](https://codecov.io/gh/Wortex17/UE4Config) 9 | [![License](https://img.shields.io/github/license/Wortex17/UE4Config)](https://raw.githubusercontent.com/Wortex17/UE4Config/master/LICENSE) 10 | 11 | ## Features 12 | 13 | * Read & parse single \*.ini config files 14 | * Serialization retains file structure and formatting of the original file by default 15 | * Evaluate the value(s) of any property across one, or multiple config files 16 | * ConfigTree classes emulate UE4s hierarchical config layers and load config files automatically 17 | * Supports new "PlatformExtension" folder structure & configs of UE4.24+ 18 | * Supports DataDrivenPlatform definitions of UE 4.27+ 19 | 20 | ## Next to come 21 | 22 | * Easier API to modify & add new properties and values 23 | 24 | ## Examples 25 | 26 | ### Evaluate a property from a single config file 27 | You can directly load and read from a single specific config \*.ini file by reading and parsing that file, before evaluating any property values. 28 | ```C# 29 | var config = new ConfigIni("DefaultGame"); 30 | 31 | //Load the configs contents from a file, via a read stream 32 | config.Read(File.OpenText("MockProject/Config/DefaultGame.ini"); 33 | 34 | //Evaluate the values and put them into a list. Should the property only have a single value, the list will have a single element. 35 | //Should the property not exist or should all its values have been deleted via config, the list will be empty. 36 | var values = new List(); 37 | config.EvaluatePropertyValues("/Script/EngineSettings.GeneralProjectSettings", "ProjectID", values); 38 | 39 | Assert.That(values, Is.EquivalentTo(new[]{"3F9D696D4363312194B0ECB2671E899F"})); 40 | ``` 41 | 42 | ### Evaluate a property from a config hierarchy 43 | You can use the virtual config tree to work with config files in memory by providing a engine and/or a project path. 44 | It can provide you branches based on target category (like Game, Editor, Input etc.) and platform you want to use. 45 | This emulates best what property values would be in which context of any Unreal Engine 4 project. 46 | ```C# 47 | //Create a new virtual config tree to allow us working with an in-memory virtual hierarchy 48 | var configTree = VirtualConfigTreeUtility.CreateVirtualConfigTree(enginePath, projectPath); 49 | 50 | //Evaluate the values and put them into a list. Should the property only have a single value, the list will have a single element. 51 | //Should the property not exist or should all its values have been deleted via config, the list will be empty. 52 | var win64Values = new List(); 53 | 54 | //Evaluate the property "LocalizationPaths" in the section "Internationalization" in the category "Game" for the topmost "Windows"-platform config in the hierarchy. 55 | //This will take all lower hierarchy layers into account. 56 | var configBranch = configTree.FetchConfigBranch("Game", "Windows"); 57 | PropertyEvaluator.Default.EvaluatePropertyValues(configBranch, "Internationalization", "LocalizationPaths", win64Values); 58 | 59 | Assert.That(win64Values, Is.EquivalentTo(new[] 60 | { 61 | "%GAMEDIR%Content/Localization/Game", 62 | "%GAMEDIR%Content/Localization/Game/Default", 63 | "%GAMEDIR%Content/Localization/Game/Win64" 64 | })); 65 | 66 | //It is also possible to ask for a specific level range within the hierarchy 67 | var filteredBranch = configBranch.FindAll((ini => ini.Reference.Domain <= ConfigDomain.Engine)); 68 | var engineDefaultWin64Values = new List(); 69 | PropertyEvaluator.Default.EvaluatePropertyValues(filteredBranch, "Internationalization", "LocalizationPaths", engineDefaultWin64Values); 70 | Assert.That(engineDefaultWin64Values, Is.EquivalentTo(new[] 71 | { 72 | "%GAMEDIR%Content/Localization/Game" 73 | })); 74 | ``` 75 | 76 | ### Modify configs in a config hierarchy 77 | You can use the virtual config tree to work with config files in memory 78 | and save them to disk after making modifications. 79 | ```C# 80 | //Create a new virtual config tree to allow us working with an in-memory virtual hierarchy 81 | var configTree = VirtualConfigTreeUtility.CreateVirtualConfigTree(enginePath, projectPath); 82 | 83 | //Acquire the target config ("Game" on Platform "Windows") we want to modify 84 | //Select the trees branch first, by providing a config category and the platform we're branching on 85 | var configBranch = configTree.FetchConfigBranch("Game", "Windows"); 86 | //Select the head config on that branch that is still in "Engine" domain 87 | var config = configBranch.SelectHeadConfig(ConfigDomain.Project); 88 | //This will be the {Project}/Config/Windows/WindowsGame.ini 89 | 90 | //We modify the config by just appending further configuration which will redefine properties 91 | config.AppendRawText("[Global]\n" + 92 | "+GUIDs=modifieda44d"); 93 | //Here we use the config ini syntax to add another value to the list 94 | 95 | //Cleanup the config before publishing it 96 | config.Sanitize(); 97 | 98 | //Publish the config and write it back 99 | configTree.PublishConfig(config); 100 | 101 | //Make sure the modified value made it into the file: 102 | //Invalidate our cache to reload the branch from disk 103 | configTree.ConfigsCache.InvalidateCache(); 104 | configBranch = configTree.FetchConfigBranch("Game", "Windows"); 105 | //Resolve the value on the branch 106 | var win64Values = new List(); 107 | PropertyEvaluator.Default.EvaluatePropertyValues(configBranch, "Global", "GUIDs", win64Values); 108 | Assert.That(win64Values, Is.EquivalentTo(new[] 109 | { 110 | "a44b", 111 | "modifieda44d" 112 | })); 113 | ``` 114 | 115 | ### Setting up a VirtualConfigTree 116 | While there is the utility method to create a virtual config tree, 117 | you may set it up manually to customize behavior. 118 | ```C# 119 | var autoTree = VirtualConfigTreeUtility.CreateVirtualConfigTree(enginePath, projectPath); 120 | 121 | //This will provide paths and a virtual hierarchy for a project+engine base path combination 122 | var configProvider = new ConfigFileProvider(); 123 | configProvider.Setup(new ConfigFileIOAdapter(), TestUtils.GetTestDataPath("MockEngineTmp"), TestUtils.GetTestDataPath("MockProjectTmp")); 124 | //Auto-detect if a project still uses the legacy Config/{Platform}/*.ini setup. 125 | configProvider.AutoDetectPlatformsUsingLegacyConfig(); 126 | //Create a DataDrivenPlatform provider that can pre-fill our platform hierarchy like UE4.27 does (based on DataDrivenPlatform configs) 127 | var dataDrivenPlatformProvider = new DataDrivenPlatformProvider(); 128 | dataDrivenPlatformProvider.Setup(configProvider); 129 | dataDrivenPlatformProvider.CollectDataDrivenPlatforms(); 130 | //Create the base tree model, based on the config hierarchy used by UE since 4.27+ 131 | var configRefTree = new ConfigReferenceTree427(); 132 | configRefTree.Setup(configProvider); 133 | //Apply any detected DataDrivenPlatforms 134 | dataDrivenPlatformProvider.RegisterDataDrivenPlatforms(configRefTree); 135 | //Create a virtual config tree to allow us working with an in-memory virtual hierarchy 136 | var configTree = new VirtualConfigTree(configRefTree); 137 | 138 | //autoTree and configTree have the exact same setup (though different instances) 139 | ``` -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/ConfigFileReferenceTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UE4Config.Hierarchy; 3 | 4 | namespace UE4Config.Tests.Hierarchy 5 | { 6 | [TestFixture] 7 | public class ConfigFileReferenceTests 8 | { 9 | [TestFixture] 10 | public class Constructor 11 | { 12 | [Test] 13 | public void When_Default() 14 | { 15 | var configFileReference = new ConfigFileReference(); 16 | 17 | Assert.That(configFileReference.Domain, Is.EqualTo(ConfigDomain.None)); 18 | Assert.That(configFileReference.Platform, Is.Null); 19 | Assert.That(configFileReference.Type, Is.Null); 20 | } 21 | 22 | [TestCase("")] 23 | [TestCase(" ")] 24 | [TestCase("\t")] 25 | public void When_WhitespaceInType(string type) 26 | { 27 | Assert.That(() => 28 | { 29 | var configFileReference = new ConfigFileReference(ConfigDomain.None, null, " "); 30 | }, Throws.ArgumentException); 31 | } 32 | 33 | [TestCase("base")] 34 | [TestCase("Base")] 35 | [TestCase("Default")] 36 | [TestCase("default")] 37 | public void When_KeywordInType(string type) 38 | { 39 | Assert.That(() => 40 | { 41 | var configFileReference = new ConfigFileReference(ConfigDomain.None, null, type); 42 | }, Throws.ArgumentException); 43 | } 44 | } 45 | 46 | [Test] 47 | public void IsPlatformConfig_WhenNoPlatform() 48 | { 49 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 50 | 51 | Assert.That(configFileReference.IsPlatformConfig, Is.False); 52 | } 53 | 54 | [Test] 55 | public void IsPlatformConfig_WhenPlatform() 56 | { 57 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, new ConfigPlatform("MyPlatform"), "MyConfig"); 58 | 59 | Assert.That(configFileReference.IsPlatformConfig, Is.True); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/ConfigHierarchyLevelTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UE4Config.Hierarchy; 3 | 4 | namespace UE4Config.Tests.Hierarchy 5 | { 6 | [TestFixture] 7 | public class ConfigHierarchyLevelTests 8 | { 9 | [TestFixture] 10 | public class Extensions 11 | { 12 | [TestCase(ConfigHierarchyLevel.Base)] 13 | [TestCase(ConfigHierarchyLevel.ProjectCategory)] 14 | [TestCase(ConfigHierarchyLevel.ProjectPlatformCategory)] 15 | public void AndLower(ConfigHierarchyLevel level) 16 | { 17 | Assert.That(level.AndLower(), Is.EqualTo(ConfigHierarchyLevelRange.AnyTo(level))); 18 | } 19 | 20 | [TestCase(ConfigHierarchyLevel.Base)] 21 | [TestCase(ConfigHierarchyLevel.ProjectCategory)] 22 | [TestCase(ConfigHierarchyLevel.ProjectPlatformCategory)] 23 | public void AndHigher(ConfigHierarchyLevel level) 24 | { 25 | Assert.That(level.AndHigher(), Is.EqualTo(ConfigHierarchyLevelRange.AnyFrom(level))); 26 | } 27 | 28 | [TestCase(ConfigHierarchyLevel.Base)] 29 | [TestCase(ConfigHierarchyLevel.ProjectCategory)] 30 | [TestCase(ConfigHierarchyLevel.ProjectPlatformCategory)] 31 | public void Exact(ConfigHierarchyLevel level) 32 | { 33 | Assert.That(level.Exact(), Is.EqualTo(ConfigHierarchyLevelRange.Exact(level))); 34 | } 35 | 36 | [TestCase(ConfigHierarchyLevel.Base, ConfigHierarchyLevel.BaseCategory)] 37 | [TestCase(ConfigHierarchyLevel.ProjectCategory, ConfigHierarchyLevel.ProjectCategory)] 38 | [TestCase(ConfigHierarchyLevel.ProjectPlatformCategory, ConfigHierarchyLevel.Base)] 39 | public void To(ConfigHierarchyLevel level, ConfigHierarchyLevel to) 40 | { 41 | Assert.That(level.To(to), Is.EqualTo(ConfigHierarchyLevelRange.FromTo(level, to))); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/IConfigPlatformExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using UE4Config.Hierarchy; 4 | 5 | namespace UE4Config.Tests.Hierarchy 6 | { 7 | [TestFixture] 8 | public class IConfigPlatformExtensionsTests 9 | { 10 | [Test] 11 | public void ResolvePlatformInheritance_WithoutInheritance() 12 | { 13 | var platform = new ConfigPlatform("MyPlatform"); 14 | var result = new List(); 15 | var expectedResult = new List() 16 | { 17 | platform 18 | }; 19 | 20 | IConfigPlatformExtensions.ResolvePlatformInheritance(platform, ref result); 21 | 22 | Assert.That(result, Is.EquivalentTo(expectedResult)); 23 | } 24 | 25 | [Test] 26 | public void ResolvePlatformInheritance_WithInheritance() 27 | { 28 | var grandParentPlatform = new ConfigPlatform("MyPlatformGrandParent"); 29 | var parentPlatform = new ConfigPlatform("MyPlatformParent", grandParentPlatform); 30 | var platform = new ConfigPlatform("MyPlatform", parentPlatform); 31 | var result = new List(); 32 | var expectedResult = new List() 33 | { 34 | grandParentPlatform, 35 | parentPlatform, 36 | platform 37 | }; 38 | 39 | IConfigPlatformExtensions.ResolvePlatformInheritance(platform, ref result); 40 | 41 | Assert.That(result, Is.EquivalentTo(expectedResult)); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/IConfigTreeExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using UE4Config.Hierarchy; 5 | 6 | namespace UE4Config.Tests.Hierarchy 7 | { 8 | [TestFixture] 9 | public class IConfigTreeExtensionsTests 10 | { 11 | class TestConfigReferenceTree : IConfigReferenceTree 12 | { 13 | public Action> OnVisitConfigRoot; 14 | public IConfigFileProvider FileProvider { get; private set; } 15 | 16 | public void Setup(IConfigFileProvider configFileProvider) 17 | { 18 | FileProvider = configFileProvider; 19 | } 20 | 21 | public void VisitConfigRoot(Action onConfig) 22 | { 23 | OnVisitConfigRoot(onConfig); 24 | } 25 | 26 | public Action> OnVisitConfigBranch; 27 | public void VisitConfigBranch(string configType, string platformIdentifier, Action onConfig) 28 | { 29 | OnVisitConfigBranch(configType, platformIdentifier, onConfig); 30 | } 31 | 32 | public IConfigPlatform GetPlatform(string platformIdentifier) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | 37 | public IConfigPlatform RegisterPlatform(string platformIdentifier, IConfigPlatform parentPlatform = null) 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | } 42 | 43 | [Test] 44 | public void GetConfigRoot() 45 | { 46 | var tree = new TestConfigReferenceTree(); 47 | var expectedResult = new ConfigFileReference(ConfigDomain.None, new ConfigPlatform("MyPlatform"), "MyConfig"); 48 | int onVisitConfigRootCount = 0; 49 | tree.OnVisitConfigRoot = action => 50 | { 51 | onVisitConfigRootCount++; 52 | action(expectedResult); 53 | }; 54 | 55 | ConfigFileReference? result = IConfigReferenceTreeExtensions.GetConfigRoot(tree); 56 | 57 | Assert.That(onVisitConfigRootCount, Is.EqualTo(1)); 58 | Assert.That(result, Is.EqualTo(expectedResult)); 59 | } 60 | 61 | [Test] 62 | public void GetConfigRoot_WithoutCallback() 63 | { 64 | var tree = new TestConfigReferenceTree(); 65 | int onVisitConfigRootCount = 0; 66 | tree.OnVisitConfigRoot = action => 67 | { 68 | onVisitConfigRootCount++; 69 | }; 70 | 71 | ConfigFileReference? result = IConfigReferenceTreeExtensions.GetConfigRoot(tree); 72 | 73 | Assert.That(onVisitConfigRootCount, Is.EqualTo(1)); 74 | Assert.That(result, Is.Null); 75 | } 76 | 77 | [Test] 78 | public void GetConfigBranch() 79 | { 80 | var tree = new TestConfigReferenceTree(); 81 | var expectedConfigType = "MyConfig"; 82 | var expectedPlatformIdentifier = "MyPlatform"; 83 | var expectedResult = new List(); 84 | tree.OnVisitConfigBranch = (configType, platformIdentifier, action) => 85 | { 86 | var configFileReference = new ConfigFileReference(ConfigDomain.None, new ConfigPlatform(platformIdentifier), 87 | configType); 88 | expectedResult.Add(configFileReference); 89 | action(configFileReference); 90 | }; 91 | 92 | var result = IConfigReferenceTreeExtensions.GetConfigBranch(tree, expectedConfigType, expectedPlatformIdentifier); 93 | 94 | Assert.That(result, Is.EquivalentTo(expectedResult)); 95 | Assert.That( 96 | result.FindIndex(reference => reference.Platform != null), 97 | Is.GreaterThanOrEqualTo(0)); 98 | Assert.That( 99 | result.FindIndex(reference => reference.Type != null), 100 | Is.GreaterThanOrEqualTo(0)); 101 | } 102 | 103 | [Test] 104 | public void GetConfigBranch_WithoutCallback() 105 | { 106 | var tree = new TestConfigReferenceTree(); 107 | tree.OnVisitConfigBranch = (configType, platformIdentifier, action) => 108 | { 109 | }; 110 | 111 | var result = IConfigReferenceTreeExtensions.GetConfigBranch(tree, "MyConfig", "MyPlatform"); 112 | 113 | Assert.That(result, Is.Empty); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/IConfigTreeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using UE4Config.Hierarchy; 5 | 6 | namespace UE4Config.Tests.Hierarchy 7 | { 8 | [TestFixture(typeof(ConfigReferenceTree427))] 9 | public class IConfigTreeTests where T : IConfigReferenceTree, new() 10 | { 11 | protected T NewTree() 12 | { 13 | return new T(); 14 | } 15 | 16 | [Test] 17 | public void Setup() 18 | { 19 | var tree = NewTree(); 20 | var fileProvider = new ConfigFileProvider(); 21 | 22 | tree.Setup(fileProvider); 23 | 24 | Assert.That(tree.FileProvider, Is.SameAs(fileProvider)); 25 | } 26 | 27 | [Test] 28 | public void MissingSetup() 29 | { 30 | var tree = NewTree(); 31 | 32 | Assert.That(tree.FileProvider, Is.Null); 33 | } 34 | 35 | [Test] 36 | public void RegisterPlatform() 37 | { 38 | var tree = NewTree(); 39 | var platformIdentifier = "GenericPlatform"; 40 | 41 | var platform = tree.RegisterPlatform(platformIdentifier); 42 | 43 | Assert.That(platform, Is.Not.Null); 44 | Assert.That(platform.Identifier, Is.EqualTo(platformIdentifier)); 45 | Assert.That(platform.ParentPlatform, Is.Null); 46 | } 47 | 48 | [Test] 49 | public void RegisterPlatformWithParent() 50 | { 51 | var tree = NewTree(); 52 | var platformIdentifier = "GenericPlatform"; 53 | var parentPlatform = tree.RegisterPlatform(platformIdentifier+"Parent"); 54 | 55 | var platform = tree.RegisterPlatform(platformIdentifier, parentPlatform); 56 | 57 | Assert.That(platform, Is.Not.Null); 58 | Assert.That(platform.Identifier, Is.EqualTo(platformIdentifier)); 59 | Assert.That(platform.ParentPlatform, Is.SameAs(parentPlatform)); 60 | } 61 | 62 | [Test] 63 | public void GetPlatform() 64 | { 65 | var tree = NewTree(); 66 | var platformIdentifier = "GenericPlatform"; 67 | var platform = tree.RegisterPlatform(platformIdentifier); 68 | 69 | var gotPlatform = tree.GetPlatform(platformIdentifier); 70 | 71 | Assert.That(gotPlatform, Is.SameAs(platform)); 72 | } 73 | 74 | [Test] 75 | public void GetPlatform_ReturnsSamePlatform() 76 | { 77 | var tree = NewTree(); 78 | var platformIdentifier = "GenericPlatform"; 79 | tree.RegisterPlatform(platformIdentifier); 80 | 81 | var gotPlatform = tree.GetPlatform(platformIdentifier); 82 | var gotPlatform2 = tree.GetPlatform(platformIdentifier); 83 | 84 | Assert.That(gotPlatform2, Is.SameAs(gotPlatform)); 85 | } 86 | 87 | [Test] 88 | public void GetPlatform_WithUnregisteredPlatform() 89 | { 90 | var tree = NewTree(); 91 | var platformIdentifier = "GenericPlatform"; 92 | 93 | var gotPlatform = tree.GetPlatform(platformIdentifier); 94 | 95 | Assert.That(gotPlatform, Is.Null); 96 | } 97 | 98 | public static IEnumerable Cases_ConfigType 99 | { 100 | get 101 | { 102 | yield return new TestCaseData(new object[]{ConfigCategory.Compat}); 103 | yield return new TestCaseData(new object[]{ConfigCategory.Editor}); 104 | yield return new TestCaseData(new object[]{ConfigCategory.Engine}); 105 | yield return new TestCaseData(new object[]{ConfigCategory.Game}); 106 | yield return new TestCaseData(new object[]{ConfigCategory.Input}); 107 | yield return new TestCaseData(new object[]{ConfigCategory.Lightmass}); 108 | yield return new TestCaseData(new object[]{ConfigCategory.Scalability}); 109 | yield return new TestCaseData(new object[]{ConfigCategory.DeviceProfiles}); 110 | yield return new TestCaseData(new object[]{ConfigCategory.EditorGameAgnostic}); 111 | yield return new TestCaseData(new object[]{ConfigCategory.EditorKeyBindings}); 112 | yield return new TestCaseData(new object[]{ConfigCategory.EditorUserSettings}); 113 | } 114 | } 115 | 116 | [Test] 117 | public void VisitConfigRoot() 118 | { 119 | var tree = NewTree(); 120 | ConfigFileReference? result = null; 121 | 122 | tree.VisitConfigRoot((configFileReference) => 123 | { 124 | result = configFileReference; 125 | }); 126 | 127 | Assert.That(result, Is.Not.Null); 128 | Assert.That(result.Value, Is.EqualTo(new ConfigFileReference(ConfigDomain.EngineBase, null, null))); 129 | } 130 | 131 | [TestCaseSource(nameof(Cases_ConfigType))] 132 | public void VisitConfigBranch_WithoutPlatformIdentifier(string configType) 133 | { 134 | var tree = NewTree(); 135 | 136 | var configBranch = new List(); 137 | tree.VisitConfigBranch(configType, null, reference => configBranch.Add(reference)); 138 | 139 | var expectedConfigBranch = new List() 140 | { 141 | // Engine/Base.ini 142 | tree.GetConfigRoot().GetValueOrDefault(), 143 | // Engine/Base*.ini 144 | new ConfigFileReference(ConfigDomain.EngineBase, null, configType), 145 | // Project/Default*.ini 146 | new ConfigFileReference(ConfigDomain.Project, null, configType), 147 | // Project/Generated*.ini 148 | new ConfigFileReference(ConfigDomain.ProjectGenerated, null, configType), 149 | }; 150 | 151 | Assert.That(configBranch, Is.EquivalentTo(expectedConfigBranch)); 152 | } 153 | 154 | void Assert_VisitConfigBranch_WithPlatformWithoutInheritance(T tree, string configType, IConfigPlatform platform, List configBranch) 155 | { 156 | var expectedConfigBranch = new List() 157 | { 158 | // Engine/Base.ini 159 | tree.GetConfigRoot().GetValueOrDefault(), 160 | // Engine/Base*.ini 161 | new ConfigFileReference(ConfigDomain.EngineBase, null, configType), 162 | // Engine/Platform/BasePlatform*.ini 163 | new ConfigFileReference(ConfigDomain.EngineBase, platform, configType), 164 | // Project/Default*.ini 165 | new ConfigFileReference(ConfigDomain.Project, null, configType), 166 | // Project/Generated*.ini 167 | new ConfigFileReference(ConfigDomain.ProjectGenerated, null, configType), 168 | // Engine/Platform/Platform*.ini 169 | new ConfigFileReference(ConfigDomain.Engine, platform, configType), 170 | // Project/Platform/Platform*.ini 171 | new ConfigFileReference(ConfigDomain.Project, platform, configType), 172 | // Project/Platform/GeneratedPlatform*.ini 173 | new ConfigFileReference(ConfigDomain.ProjectGenerated, platform, configType), 174 | }; 175 | 176 | Assert.That(configBranch, Is.EquivalentTo(expectedConfigBranch)); 177 | } 178 | 179 | [TestCaseSource(nameof(Cases_ConfigType))] 180 | public void VisitConfigBranch_WithUnregisteredPlatformWithoutInheritance(string configType) 181 | { 182 | var tree = NewTree(); 183 | var platformIdentifier = "GenericPlatform"; 184 | var configBranch = new List(); 185 | tree.VisitConfigBranch(configType, platformIdentifier, reference => configBranch.Add(reference)); 186 | var platform = tree.GetPlatform(platformIdentifier); 187 | 188 | Assert_VisitConfigBranch_WithPlatformWithoutInheritance(tree, configType, platform, configBranch); 189 | } 190 | 191 | [TestCaseSource(nameof(Cases_ConfigType))] 192 | public void VisitConfigBranch_WithRegisteredPlatformWithoutInheritance(string configType) 193 | { 194 | var tree = NewTree(); 195 | var platformIdentifier = "GenericPlatform"; 196 | var configBranch = new List(); 197 | var platform = tree.RegisterPlatform(platformIdentifier); 198 | 199 | tree.VisitConfigBranch(configType, platformIdentifier, reference => configBranch.Add(reference)); 200 | 201 | Assert_VisitConfigBranch_WithPlatformWithoutInheritance(tree, configType, platform, configBranch); 202 | } 203 | 204 | [TestCaseSource(nameof(Cases_ConfigType))] 205 | public void VisitConfigBranch_WithRegisteredPlatformWithInheritance(string configType) 206 | { 207 | var tree = NewTree(); 208 | var platformIdentifier = "GenericPlatform"; 209 | var configBranch = new List(); 210 | var grandParentPlatform = tree.RegisterPlatform(platformIdentifier+"GrandParent"); 211 | var parentPlatform = tree.RegisterPlatform(platformIdentifier+"Parent", grandParentPlatform); 212 | var platform = tree.RegisterPlatform(platformIdentifier, parentPlatform); 213 | 214 | tree.VisitConfigBranch(configType, platformIdentifier, reference => configBranch.Add(reference)); 215 | 216 | var expectedConfigBranch = new List() 217 | { 218 | // Engine/Base.ini 219 | tree.GetConfigRoot().GetValueOrDefault(), 220 | // Engine/Base*.ini 221 | new ConfigFileReference(ConfigDomain.EngineBase, null, configType), 222 | // Engine/Platform/BasePlatform*.ini 223 | new ConfigFileReference(ConfigDomain.EngineBase, grandParentPlatform, configType), 224 | new ConfigFileReference(ConfigDomain.EngineBase, parentPlatform, configType), 225 | new ConfigFileReference(ConfigDomain.EngineBase, platform, configType), 226 | // Project/Default*.ini 227 | new ConfigFileReference(ConfigDomain.Project, null, configType), 228 | // Project/Generated*.ini 229 | new ConfigFileReference(ConfigDomain.ProjectGenerated, null, configType), 230 | // Engine/Platform/Platform*.ini 231 | new ConfigFileReference(ConfigDomain.Engine, grandParentPlatform, configType), 232 | // Project/Platform/Platform*.ini 233 | new ConfigFileReference(ConfigDomain.Project, grandParentPlatform, configType), 234 | // Project/Platform/GeneratedPlatform*.ini 235 | new ConfigFileReference(ConfigDomain.ProjectGenerated, grandParentPlatform, configType), 236 | // Engine/Platform/Platform*.ini 237 | new ConfigFileReference(ConfigDomain.Engine, parentPlatform, configType), 238 | // Project/Platform/Platform*.ini 239 | new ConfigFileReference(ConfigDomain.Project, parentPlatform, configType), 240 | // Project/Platform/GeneratedPlatform*.ini 241 | new ConfigFileReference(ConfigDomain.ProjectGenerated, parentPlatform, configType), 242 | // Engine/Platform/Platform*.ini 243 | new ConfigFileReference(ConfigDomain.Engine, platform, configType), 244 | // Project/Platform/Platform*.ini 245 | new ConfigFileReference(ConfigDomain.Project, platform, configType), 246 | // Project/Platform/GeneratedPlatform*.ini 247 | new ConfigFileReference(ConfigDomain.ProjectGenerated, platform, configType), 248 | }; 249 | //TODO: Missing User Layers 250 | Assert.That(configBranch, Is.EquivalentTo(expectedConfigBranch)); 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/VirtualConfigCacheTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UE4Config.Hierarchy; 3 | using UE4Config.Parsing; 4 | 5 | namespace UE4Config.Tests.Hierarchy 6 | { 7 | [TestFixture] 8 | public class VirtualConfigCacheTests 9 | { 10 | [Test] 11 | public void Constructor() 12 | { 13 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 14 | 15 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 16 | 17 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 18 | Assert.That(virtualConfigCache.ConfigIni, Is.Null); 19 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Unknown)); 20 | } 21 | 22 | [TestFixture] 23 | public class SetConfigIni 24 | { 25 | [Test] 26 | public void When_WasNotLoaded() 27 | { 28 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 29 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 30 | var configIni = new ConfigIni(); 31 | 32 | virtualConfigCache.SetConfigIni(configIni, false); 33 | 34 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 35 | Assert.That(virtualConfigCache.ConfigIni, Is.SameAs(configIni)); 36 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Created)); 37 | } 38 | 39 | [Test] 40 | public void When_WasLoaded() 41 | { 42 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 43 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 44 | var configIni = new ConfigIni(); 45 | 46 | virtualConfigCache.SetConfigIni(configIni, true); 47 | 48 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 49 | Assert.That(virtualConfigCache.ConfigIni, Is.SameAs(configIni)); 50 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Loaded)); 51 | } 52 | 53 | [Test] 54 | public void When_NullWasNotLoaded() 55 | { 56 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 57 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 58 | 59 | virtualConfigCache.SetConfigIni(null, false); 60 | 61 | Assert.That(virtualConfigCache.ConfigIni, Is.Null); 62 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Created)); 63 | } 64 | 65 | [Test] 66 | public void When_NullWasLoaded() 67 | { 68 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 69 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 70 | 71 | virtualConfigCache.SetConfigIni(null, true); 72 | 73 | Assert.That(virtualConfigCache.ConfigIni, Is.Null); 74 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Loaded)); 75 | } 76 | 77 | [Test] 78 | public void When_FileReferenceDiffers() 79 | { 80 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 81 | var otherConfigFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig2"); 82 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 83 | var configIni = new ConfigIni("MyConfig2", otherConfigFileReference); 84 | 85 | virtualConfigCache.SetConfigIni(configIni, false); 86 | 87 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 88 | Assert.That(configIni.Reference, Is.EqualTo(otherConfigFileReference)); 89 | Assert.That(virtualConfigCache.ConfigIni, Is.SameAs(configIni)); 90 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Created)); 91 | } 92 | } 93 | 94 | [Test] 95 | public void InvalidateCache() 96 | { 97 | var configFileReference = new ConfigFileReference(ConfigDomain.EngineBase, null, "MyConfig"); 98 | var virtualConfigCache = new VirtualConfigCache(configFileReference); 99 | var configIni = new ConfigIni(); 100 | virtualConfigCache.SetConfigIni(configIni, false); 101 | 102 | virtualConfigCache.InvalidateCache(); 103 | 104 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 105 | Assert.That(virtualConfigCache.ConfigIni, Is.Null); 106 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Unknown)); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/VirtualConfigTreeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using UE4Config.Hierarchy; 4 | using UE4Config.Parsing; 5 | 6 | namespace UE4Config.Tests.Hierarchy 7 | { 8 | [TestFixture] 9 | public class VirtualConfigTreeTests 10 | { 11 | class MockConfigFileProvider : IConfigFileProvider 12 | { 13 | public delegate bool OnLoadOrCreateConfigDelegate(ConfigFileReference configFileReference, 14 | out ConfigIni configIni); 15 | public OnLoadOrCreateConfigDelegate OnLoadOrCreateConfig; 16 | 17 | public delegate void OnSaveConfigDelegate(ConfigFileReference configFileReference, 18 | ConfigIni configIni); 19 | public OnSaveConfigDelegate OnSaveConfig; 20 | 21 | public IConfigFileIOAdapter FileIOAdapter { get; } 22 | public string EnginePath { get; } 23 | public string ProjectPath { get; } 24 | public bool IsSetup { get; } 25 | public void Setup(IConfigFileIOAdapter fileIOAdapter, string enginePath, string projectPath) 26 | { 27 | throw new System.NotImplementedException(); 28 | } 29 | 30 | public string ResolveConfigFilePath(ConfigFileReference reference) 31 | { 32 | throw new System.NotImplementedException(); 33 | } 34 | 35 | public bool LoadOrCreateConfig(ConfigFileReference configFileReference, out ConfigIni configIni) 36 | { 37 | return OnLoadOrCreateConfig(configFileReference, out configIni); 38 | } 39 | 40 | public bool LoadOrCreateDataDrivenPlatformConfig(string platformIdentifier, out ConfigIni configIni) 41 | { 42 | throw new System.NotImplementedException(); 43 | } 44 | 45 | public void SaveConfig(ConfigFileReference configFileReference, ConfigIni configIni) 46 | { 47 | OnSaveConfig(configFileReference, configIni); 48 | } 49 | } 50 | 51 | [Test] 52 | public void Constructor() 53 | { 54 | var configFileProvider = new MockConfigFileProvider(); 55 | var configRefTree = new ConfigReferenceTree427(); 56 | configRefTree.Setup(configFileProvider); 57 | 58 | var virtualConfigTree = new VirtualConfigTree(configRefTree); 59 | 60 | Assert.That(virtualConfigTree.ReferenceTree, Is.SameAs(configRefTree)); 61 | Assert.That(virtualConfigTree.FileProvider, Is.SameAs(configRefTree.FileProvider)); 62 | Assert.That(virtualConfigTree.ConfigsCache, Is.Not.Null); 63 | } 64 | 65 | [Test] 66 | public void FetchConfigRoot() 67 | { 68 | var configFileProvider = new MockConfigFileProvider(); 69 | var configRefTree = new ConfigReferenceTree427(); 70 | configRefTree.Setup(configFileProvider); 71 | var virtualConfigTree = new VirtualConfigTree(configRefTree); 72 | var rootConfig = new ConfigIni(); 73 | virtualConfigTree.ConfigsCache.Peek((ConfigFileReference)virtualConfigTree.ReferenceTree.GetConfigRoot()).SetConfigIni(rootConfig, true); 74 | 75 | var result = virtualConfigTree.FetchConfigRoot(); 76 | 77 | Assert.That(result, Is.SameAs(rootConfig)); 78 | } 79 | 80 | [Test] 81 | public void FetchConfigBranch() 82 | { 83 | var configFileProvider = new MockConfigFileProvider(); 84 | var configRefTree = new ConfigReferenceTree427(); 85 | configRefTree.Setup(configFileProvider); 86 | var virtualConfigTree = new VirtualConfigTree(configRefTree); 87 | var refBranch = configRefTree.GetConfigBranch("MyConfig", null); 88 | var expectedBranch = new List(); 89 | foreach (var refBranchItem in refBranch) 90 | { 91 | var configForItem = new ConfigIni(); 92 | expectedBranch.Add(configForItem); 93 | virtualConfigTree.ConfigsCache.Peek(refBranchItem).SetConfigIni(configForItem, true); 94 | } 95 | 96 | var result = virtualConfigTree.FetchConfigBranch("MyConfig", null); 97 | 98 | Assert.That(result, Is.EquivalentTo(expectedBranch)); 99 | } 100 | 101 | [Test] 102 | public void PublishConfig() 103 | { 104 | var configFileProvider = new MockConfigFileProvider(); 105 | var configRefTree = new ConfigReferenceTree427(); 106 | configRefTree.Setup(configFileProvider); 107 | var virtualConfigTree = new VirtualConfigTree(configRefTree); 108 | var configFileReference = new ConfigFileReference(ConfigDomain.Project, null, "MyConfig"); 109 | var editConfig = new ConfigIni("MyConfig", configFileReference); 110 | editConfig.AppendRawText("foobar"); //Make the config contain anything 111 | 112 | ConfigIni configToSave = null; 113 | int callCountOnSaveConfig = 0; 114 | configFileProvider.OnSaveConfig = (ConfigFileReference reference, ConfigIni ini) => 115 | { 116 | callCountOnSaveConfig++; 117 | configToSave = ini; 118 | }; 119 | 120 | virtualConfigTree.PublishConfig(editConfig); 121 | var cached = virtualConfigTree.ConfigsCache.Peek(editConfig.Reference); 122 | 123 | Assert.That(callCountOnSaveConfig, Is.EqualTo(1)); 124 | Assert.That(configToSave, Is.SameAs(editConfig)); 125 | Assert.That(cached.ConfigIni, Is.SameAs(editConfig)); 126 | Assert.That(cached.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Loaded)); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Hierarchy/VirtualConfigsCacheTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UE4Config.Hierarchy; 3 | using UE4Config.Parsing; 4 | 5 | namespace UE4Config.Tests.Hierarchy 6 | { 7 | [TestFixture] 8 | public class VirtualConfigsCacheTests 9 | { 10 | class MockConfigFileProvider : IConfigFileProvider 11 | { 12 | public delegate bool OnLoadOrCreateConfigDelegate(ConfigFileReference configFileReference, 13 | out ConfigIni configIni); 14 | 15 | public OnLoadOrCreateConfigDelegate OnLoadOrCreateConfig; 16 | 17 | public IConfigFileIOAdapter FileIOAdapter { get; } 18 | public string EnginePath { get; } 19 | public string ProjectPath { get; } 20 | public bool IsSetup { get; } 21 | public void Setup(IConfigFileIOAdapter fileIOAdapter, string enginePath, string projectPath) 22 | { 23 | throw new System.NotImplementedException(); 24 | } 25 | 26 | public string ResolveConfigFilePath(ConfigFileReference reference) 27 | { 28 | throw new System.NotImplementedException(); 29 | } 30 | 31 | public bool LoadOrCreateConfig(ConfigFileReference configFileReference, out ConfigIni configIni) 32 | { 33 | return OnLoadOrCreateConfig(configFileReference, out configIni); 34 | } 35 | 36 | public bool LoadOrCreateDataDrivenPlatformConfig(string platformIdentifier, out ConfigIni configIni) 37 | { 38 | throw new System.NotImplementedException(); 39 | } 40 | 41 | public void SaveConfig(ConfigFileReference configFileReference, ConfigIni configIni) 42 | { 43 | throw new System.NotImplementedException(); 44 | } 45 | } 46 | 47 | [Test] 48 | public void Peek() 49 | { 50 | var virtualConfigsCache = new VirtualConfigsCache(); 51 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 52 | 53 | var virtualConfigCache = virtualConfigsCache.Peek(configFileReference); 54 | 55 | Assert.That(virtualConfigCache.FileReference, Is.EqualTo(configFileReference)); 56 | Assert.That(virtualConfigCache.ConfigIni, Is.Null); 57 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Unknown)); 58 | } 59 | 60 | [Test] 61 | public void Peek_Twice() 62 | { 63 | var virtualConfigsCache = new VirtualConfigsCache(); 64 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 65 | 66 | var virtualConfigCache1 = virtualConfigsCache.Peek(configFileReference); 67 | var virtualConfigCache2 = virtualConfigsCache.Peek(configFileReference); 68 | 69 | Assert.That(virtualConfigCache1, Is.SameAs(virtualConfigCache2)); 70 | } 71 | 72 | [Test] 73 | public void Peek_DifferentTwice() 74 | { 75 | var virtualConfigsCache = new VirtualConfigsCache(); 76 | var configFileReference1 = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig1"); 77 | var configFileReference2 = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig2"); 78 | 79 | var virtualConfigCache1 = virtualConfigsCache.Peek(configFileReference1); 80 | var virtualConfigCache2 = virtualConfigsCache.Peek(configFileReference2); 81 | 82 | Assert.That(virtualConfigCache1, Is.Not.SameAs(virtualConfigCache2)); 83 | } 84 | 85 | [TestFixture] 86 | public class GetOrLoadConfig 87 | { 88 | 89 | [Test] 90 | public void When_CannotLoadExisting() 91 | { 92 | var virtualConfigsCache = new VirtualConfigsCache(); 93 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 94 | var configFileProvider = new MockConfigFileProvider(); 95 | var expectedConfig = new ConfigIni(); 96 | bool expectedConfigWasLoaded = false; 97 | int callCount_OnLoadOrCreateConfig = 0; 98 | configFileProvider.OnLoadOrCreateConfig = (ConfigFileReference reference, out ConfigIni ini) => 99 | { 100 | callCount_OnLoadOrCreateConfig++; 101 | ini = expectedConfig; 102 | return expectedConfigWasLoaded; 103 | }; 104 | 105 | var result = virtualConfigsCache.GetOrLoadConfig(configFileReference, configFileProvider); 106 | var virtualConfigCache = virtualConfigsCache.Peek(configFileReference); 107 | 108 | Assert.That(callCount_OnLoadOrCreateConfig, Is.EqualTo(1)); 109 | Assert.That(result, Is.SameAs(expectedConfig)); 110 | Assert.That(result, Is.SameAs(virtualConfigCache.ConfigIni)); 111 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Created)); 112 | } 113 | 114 | [Test] 115 | public void When_LoadsExisting() 116 | { 117 | var virtualConfigsCache = new VirtualConfigsCache(); 118 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 119 | var configFileProvider = new MockConfigFileProvider(); 120 | var expectedConfig = new ConfigIni(); 121 | bool expectedConfigWasLoaded = true; 122 | int callCount_OnLoadOrCreateConfig = 0; 123 | configFileProvider.OnLoadOrCreateConfig = (ConfigFileReference reference, out ConfigIni ini) => 124 | { 125 | callCount_OnLoadOrCreateConfig++; 126 | ini = expectedConfig; 127 | return expectedConfigWasLoaded; 128 | }; 129 | 130 | var result = virtualConfigsCache.GetOrLoadConfig(configFileReference, configFileProvider); 131 | var virtualConfigCache = virtualConfigsCache.Peek(configFileReference); 132 | 133 | Assert.That(callCount_OnLoadOrCreateConfig, Is.EqualTo(1)); 134 | Assert.That(result, Is.SameAs(expectedConfig)); 135 | Assert.That(result, Is.SameAs(virtualConfigCache.ConfigIni)); 136 | Assert.That(virtualConfigCache.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Loaded)); 137 | } 138 | 139 | [Test] 140 | public void When_CalledTwice() 141 | { 142 | var virtualConfigsCache = new VirtualConfigsCache(); 143 | var configFileReference = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig"); 144 | var configFileProvider = new MockConfigFileProvider(); 145 | var expectedConfig = new ConfigIni(); 146 | bool expectedConfigWasLoaded = true; 147 | int callCount_OnLoadOrCreateConfig = 0; 148 | configFileProvider.OnLoadOrCreateConfig = (ConfigFileReference reference, out ConfigIni ini) => 149 | { 150 | callCount_OnLoadOrCreateConfig++; 151 | ini = expectedConfig; 152 | return expectedConfigWasLoaded; 153 | }; 154 | 155 | var result1 = virtualConfigsCache.GetOrLoadConfig(configFileReference, configFileProvider); 156 | var result2 = virtualConfigsCache.GetOrLoadConfig(configFileReference, configFileProvider); 157 | 158 | Assert.That(callCount_OnLoadOrCreateConfig, Is.EqualTo(1)); 159 | Assert.That(result1, Is.SameAs(expectedConfig)); 160 | Assert.That(result2, Is.SameAs(expectedConfig)); 161 | } 162 | } 163 | 164 | [Test] 165 | public void InvalidateCache() 166 | { 167 | var virtualConfigsCache = new VirtualConfigsCache(); 168 | var configFileReference1 = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig1"); 169 | var configFileReference2 = new ConfigFileReference(ConfigDomain.Engine, null, "MyConfig2"); 170 | 171 | var configFileProvider = new MockConfigFileProvider(); 172 | configFileProvider.OnLoadOrCreateConfig = (ConfigFileReference reference, out ConfigIni ini) => 173 | { 174 | ini = new ConfigIni(); 175 | return true; 176 | }; 177 | 178 | virtualConfigsCache.GetOrLoadConfig(configFileReference1, configFileProvider); 179 | virtualConfigsCache.GetOrLoadConfig(configFileReference2, configFileProvider); 180 | 181 | virtualConfigsCache.InvalidateCache(); 182 | 183 | var virtualConfigCache1 = virtualConfigsCache.Peek(configFileReference1); 184 | var virtualConfigCache2 = virtualConfigsCache.Peek(configFileReference2); 185 | 186 | Assert.That(virtualConfigCache1.ConfigIni, Is.Null); 187 | Assert.That(virtualConfigCache1.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Unknown)); 188 | Assert.That(virtualConfigCache2.ConfigIni, Is.Null); 189 | Assert.That(virtualConfigCache2.FileState, Is.EqualTo(VirtualConfigCache.ConfigFileState.Unknown)); 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/ConfigIniWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using NUnit.Framework; 5 | using UE4Config.Parsing; 6 | 7 | namespace UE4Config.Tests.Parsing 8 | { 9 | [TestFixture] 10 | class ConfigIniWriterTests 11 | { 12 | 13 | struct ConfigIniCase 14 | { 15 | public string CaseName; 16 | public string Content; 17 | public bool NoLEInformed; 18 | } 19 | 20 | struct LineEndingCase 21 | { 22 | public string CaseName; 23 | public LineEnding Content; 24 | public string ContentAsString => Content.AsString(); 25 | } 26 | 27 | static IEnumerable Cases_LineEndings 28 | { 29 | get 30 | { 31 | yield return new LineEndingCase() 32 | { 33 | CaseName = "Unix", 34 | Content = LineEnding.Unix 35 | }; 36 | yield return new LineEndingCase() 37 | { 38 | CaseName = "Windows", 39 | Content = LineEnding.Windows 40 | }; 41 | yield return new LineEndingCase() 42 | { 43 | CaseName = "Mac", 44 | Content = LineEnding.Mac 45 | }; 46 | } 47 | } 48 | 49 | static IEnumerable Get_Cases_ConfigInis(string lineEnding) 50 | { 51 | yield return new ConfigIniCase() 52 | { 53 | CaseName = "Empty", 54 | Content = "", 55 | NoLEInformed = true 56 | }; 57 | yield return new ConfigIniCase() 58 | { 59 | CaseName = "Sole Header (Leading LE)", 60 | Content = "[MySection]", 61 | NoLEInformed = true 62 | }; 63 | yield return new ConfigIniCase() 64 | { 65 | CaseName = "One Section", 66 | Content = "[MySection]"+lineEnding+ 67 | "MyProp=4" 68 | }; 69 | yield return new ConfigIniCase() 70 | { 71 | CaseName = "One Section + Leading Comment", 72 | Content = ";MyComment"+lineEnding+ 73 | "[MySection]"+lineEnding+ 74 | "MyProp=4" 75 | }; 76 | yield return new ConfigIniCase() 77 | { 78 | CaseName = "One Section + Closing Comment", 79 | Content = "[MySection]"+lineEnding+ 80 | "MyProp=4"+lineEnding+ 81 | ";MyComment" 82 | }; 83 | yield return new ConfigIniCase() 84 | { 85 | CaseName = "One Section with superfl. newline", 86 | Content = "[MySection]"+lineEnding+ 87 | "MyProp=4"+lineEnding+ 88 | lineEnding+ 89 | "MyProp3=44" 90 | }; 91 | yield return new ConfigIniCase() 92 | { 93 | CaseName = "Two Sections", 94 | Content = "[MySection]"+lineEnding+ 95 | "MyProp=4"+lineEnding+ 96 | lineEnding+ 97 | "[MySection2]"+lineEnding+ 98 | "MyProp2=4" 99 | }; 100 | } 101 | 102 | [TestFixture] 103 | class AppendQuirkFileEnding 104 | { 105 | private ConfigIniWriter Writer; 106 | 107 | [SetUp] 108 | public void Setup() 109 | { 110 | Writer = new ConfigIniWriter(new StringWriter()); 111 | Writer.AppendQuirkFileEnding = true; 112 | } 113 | 114 | static IEnumerable Cases_When_HasDoubleLineEndingAtEndOfFile 115 | { 116 | get 117 | { 118 | foreach (var lineEndingCase in Cases_LineEndings) 119 | { 120 | foreach (var configIniCase in Get_Cases_ConfigInis(lineEndingCase.ContentAsString)) 121 | { 122 | string caseSuffix = lineEndingCase.ContentAsString + lineEndingCase.ContentAsString; 123 | yield return new TestCaseData( 124 | new object[] { configIniCase.Content + caseSuffix }) 125 | .SetName(configIniCase.CaseName + $"(LE:{lineEndingCase.CaseName})"); 126 | } 127 | } 128 | } 129 | } 130 | 131 | [TestCaseSource(nameof(Cases_When_HasDoubleLineEndingAtEndOfFile))] 132 | public void When_HasDoubleLineEndingAtEndOfFile(string original) 133 | { 134 | var config = new ConfigIni(); 135 | config.Read(new StringReader(original)); 136 | 137 | config.Write(Writer); 138 | Assert.That(Writer.ToString(), Is.EqualTo(original)); 139 | } 140 | 141 | static IEnumerable Cases_When_HasTooFewLineEndingAtEndOfFile 142 | { 143 | get 144 | { 145 | foreach (var lineEndingCase in Cases_LineEndings) 146 | { 147 | foreach (var configIniCase in Get_Cases_ConfigInis(lineEndingCase.ContentAsString)) 148 | { 149 | const string caseVariantName = "No LE@EOF"; 150 | const string caseSuffix = ""; 151 | yield return new TestCaseData( 152 | new object[] { configIniCase.Content + caseSuffix, lineEndingCase.Content, configIniCase.NoLEInformed }) 153 | .SetName(configIniCase.CaseName + ", " + caseVariantName + $" (LE:{lineEndingCase.CaseName})"); 154 | } 155 | foreach (var configIniCase in Get_Cases_ConfigInis(lineEndingCase.ContentAsString)) 156 | { 157 | const string caseVariantName = "Single LE@EOF"; 158 | string caseSuffix = lineEndingCase.ContentAsString; 159 | yield return new TestCaseData( 160 | new object[] { configIniCase.Content + caseSuffix, lineEndingCase.Content, configIniCase.NoLEInformed }) 161 | .SetName(configIniCase.CaseName + ", " + caseVariantName + $" (LE:{lineEndingCase.CaseName})"); 162 | } 163 | } 164 | } 165 | } 166 | static IEnumerable Cases_When_HasTooManyLineEndingAtEndOfFile 167 | { 168 | get 169 | { 170 | foreach (var lineEndingCase in Cases_LineEndings) 171 | { 172 | foreach (var configIniCase in Get_Cases_ConfigInis(lineEndingCase.ContentAsString)) 173 | { 174 | const string caseVariantName = "Three LEs@EOF"; 175 | string caseSuffix = lineEndingCase.ContentAsString+lineEndingCase.ContentAsString+lineEndingCase.ContentAsString; 176 | yield return new TestCaseData( 177 | new object[] { configIniCase.Content + caseSuffix, lineEndingCase.Content, configIniCase.NoLEInformed }) 178 | .SetName(configIniCase.CaseName + ", " + caseVariantName + $" (LE:{lineEndingCase.CaseName})"); 179 | } 180 | } 181 | } 182 | } 183 | 184 | [TestCaseSource(nameof(Cases_When_HasTooFewLineEndingAtEndOfFile))] 185 | [TestCaseSource(nameof(Cases_When_HasTooManyLineEndingAtEndOfFile))] 186 | public void When_HasNoDoubleLineEndingAtEndOfFile(string original, LineEnding lineEnding, bool noLEInformed) 187 | { 188 | var config = new ConfigIni(); 189 | config.Read(new StringReader(original)); 190 | if (noLEInformed) 191 | { 192 | Writer.LineEnding = lineEnding; 193 | } 194 | 195 | var leStr = lineEnding.AsString(); 196 | config.Write(Writer); 197 | var writtenString = Writer.ToString(); 198 | Assert.That(writtenString.Length - original.Length, Is.InRange(0, leStr.Length*2)); 199 | Assert.That(writtenString.Substring(writtenString.Length-leStr.Length*2), Is.EqualTo(leStr+leStr)); 200 | Assert.That(writtenString.Substring(0, original.Length), Is.EqualTo(original)); 201 | } 202 | } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/InstructionTokenTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using NUnit.Framework; 4 | using UE4Config.Parsing; 5 | 6 | namespace UE4Config.Tests.Parsing 7 | { 8 | [TestFixture] 9 | class InstructionTokenTests 10 | { 11 | [TestFixture] 12 | class Constructor 13 | { 14 | [Test] 15 | public void When_WithoutArguments() 16 | { 17 | var instruction = new InstructionToken(); 18 | Assert.That(instruction.InstructionType, Is.EqualTo(InstructionType.Set)); 19 | Assert.That(instruction.Key, Is.Null); 20 | Assert.That(instruction.Value, Is.Null); 21 | Assert.That(instruction.LineEnding, Is.EqualTo(LineEnding.Unknown)); 22 | } 23 | 24 | static IEnumerable Cases_When_WithTypeAndKey 25 | { 26 | get 27 | { 28 | var instructionTypes = (InstructionType[])Enum.GetValues(typeof(InstructionType)); 29 | foreach (var instructionType in instructionTypes) 30 | { 31 | yield return new TestCaseData(new object[] { instructionType }); 32 | } 33 | } 34 | } 35 | 36 | [TestCaseSource(nameof(Cases_When_WithTypeAndKey))] 37 | public void When_WithTypeAndKey(InstructionType type) 38 | { 39 | var instruction = new InstructionToken(type, "key"); 40 | Assert.That(instruction.InstructionType, Is.EqualTo(type)); 41 | Assert.That(instruction.Key, Is.EqualTo("key")); 42 | Assert.That(instruction.Value, Is.Null); 43 | Assert.That(instruction.LineEnding, Is.EqualTo(LineEnding.Unknown)); 44 | } 45 | 46 | [TestCaseSource(nameof(Cases_When_WithTypeAndKey))] 47 | public void When_WithTypeKeyAndValue(InstructionType type) 48 | { 49 | var instruction = new InstructionToken(type, "key", "value"); 50 | Assert.That(instruction.InstructionType, Is.EqualTo(type)); 51 | Assert.That(instruction.Key, Is.EqualTo("key")); 52 | Assert.That(instruction.Value, Is.EqualTo("value")); 53 | Assert.That(instruction.LineEnding, Is.EqualTo(LineEnding.Unknown)); 54 | } 55 | 56 | static IEnumerable Cases_When_WithTypeKeyAndLineEnding 57 | { 58 | get 59 | { 60 | var instructionTypes = (InstructionType[])Enum.GetValues(typeof(InstructionType)); 61 | var lineEndings = (LineEnding[])Enum.GetValues(typeof(LineEnding)); 62 | foreach (var instructionType in instructionTypes) 63 | { 64 | foreach (var lineEnding in lineEndings) 65 | { 66 | yield return new TestCaseData(new object[] { instructionType, lineEnding }); 67 | } 68 | } 69 | } 70 | } 71 | 72 | [TestCaseSource(nameof(Cases_When_WithTypeKeyAndLineEnding))] 73 | public void When_WithTypeKeyAndLineEnding(InstructionType type, LineEnding lineEnding) 74 | { 75 | var instruction = new InstructionToken(type, "key", lineEnding); 76 | Assert.That(instruction.InstructionType, Is.EqualTo(type)); 77 | Assert.That(instruction.Key, Is.EqualTo("key")); 78 | Assert.That(instruction.Value, Is.Null); 79 | Assert.That(instruction.LineEnding, Is.EqualTo(lineEnding)); 80 | } 81 | 82 | [TestCaseSource(nameof(Cases_When_WithTypeKeyAndLineEnding))] 83 | public void When_WithTypeKeyLineEndingAndValue(InstructionType type, LineEnding lineEnding) 84 | { 85 | var instruction = new InstructionToken(type, "key", "value", lineEnding); 86 | Assert.That(instruction.InstructionType, Is.EqualTo(type)); 87 | Assert.That(instruction.Key, Is.EqualTo("key")); 88 | Assert.That(instruction.Value, Is.EqualTo("value")); 89 | Assert.That(instruction.LineEnding, Is.EqualTo(lineEnding)); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/InstructionTypeTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using NUnit.Framework; 3 | using UE4Config.Parsing; 4 | 5 | namespace UE4Config.Tests.Parsing 6 | { 7 | [TestFixture] 8 | class InstructionTypeTests 9 | { 10 | [TestFixture] 11 | class AsPrefixString 12 | { 13 | [TestCase(InstructionType.Set, "")] 14 | [TestCase(InstructionType.Add, "+")] 15 | [TestCase(InstructionType.AddForce, ".")] 16 | [TestCase(InstructionType.Remove, "-")] 17 | [TestCase(InstructionType.RemoveAll, "!")] 18 | public void When_ValidEnum(InstructionType instructionType, string expectedOutput) 19 | { 20 | Assert.That(instructionType.AsPrefixString(), Is.EqualTo(expectedOutput)); 21 | } 22 | 23 | [Test] 24 | public void When_InvalidEnum() 25 | { 26 | Assert.That(() => { ((InstructionType)999).AsPrefixString(); }, Throws.TypeOf()); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/LineEndingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using NUnit.Framework; 5 | using UE4Config.Parsing; 6 | 7 | namespace UE4Config.Tests.Parsing 8 | { 9 | [TestFixture] 10 | class LineEndingTests 11 | { 12 | [TestFixture] 13 | class AsString 14 | { 15 | [TestCase(LineEnding.None, "")] 16 | [TestCase(LineEnding.Unix, "\n")] 17 | [TestCase(LineEnding.Windows, "\r\n")] 18 | [TestCase(LineEnding.Mac, "\r")] 19 | public void When_ValidEnum(LineEnding lineEnding, string expectedOutput) 20 | { 21 | Assert.That(lineEnding.AsString(), Is.EqualTo(expectedOutput)); 22 | } 23 | 24 | [Test] 25 | public void When_Unknown_ResolvesToSystemDefault() 26 | { 27 | Assert.That(LineEnding.Unknown.AsString(), Is.EqualTo(Environment.NewLine)); 28 | } 29 | 30 | [Test] 31 | public void When_InvalidEnum() 32 | { 33 | Assert.That(() => { ((LineEnding)999).AsString(); }, Throws.TypeOf()); 34 | } 35 | } 36 | 37 | [TestFixture] 38 | class Write 39 | { 40 | [Test] 41 | public void When_Unknown_WritesWritersNewLine() 42 | { 43 | var writer = new StringWriter(); 44 | LineEnding.Unknown.WriteTo(writer); 45 | Assert.That(writer.ToString(), Is.EqualTo(writer.NewLine)); 46 | } 47 | 48 | [Test] 49 | public void When_UnknownWithModifiedWriter_WritesWritersNewLine() 50 | { 51 | var writer = new StringWriter(); 52 | writer.NewLine = "CustomNewLine\t"; 53 | LineEnding.Unknown.WriteTo(writer); 54 | Assert.That(writer.ToString(), Is.EqualTo(writer.NewLine)); 55 | } 56 | 57 | [TestCase(LineEnding.None, "")] 58 | [TestCase(LineEnding.Unix, "\n")] 59 | [TestCase(LineEnding.Windows, "\r\n")] 60 | [TestCase(LineEnding.Mac, "\r")] 61 | public void When_SpecificLineEnding(LineEnding lineEnding, string expectedOutput) 62 | { 63 | var writer = new StringWriter(); 64 | lineEnding.WriteTo(writer); 65 | Assert.That(writer.ToString(), Is.EqualTo(expectedOutput)); 66 | } 67 | 68 | [Test] 69 | public void When_InvalidEnum() 70 | { 71 | var writer = new StringWriter(); 72 | Assert.That(() => { ((LineEnding)999).WriteTo(writer); }, Throws.TypeOf()); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/LineTokenTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.IO; 4 | using NUnit.Framework; 5 | using UE4Config.Parsing; 6 | 7 | namespace UE4Config.Tests.Parsing 8 | { 9 | [TestFixture] 10 | class LineTokenTests 11 | { 12 | static TextToken CreateTextToken(string text) 13 | { 14 | return new TextToken() {Text = text}; 15 | } 16 | 17 | static TextToken CreateTextToken(string text, LineEnding lineEnding) 18 | { 19 | return new TextToken() { Text = text, LineEnding = lineEnding}; 20 | } 21 | 22 | public static IEnumerable Cases_Write_TextToken 23 | { 24 | get 25 | { 26 | yield return new TestCaseData(new object[] { CreateTextToken("foobar"), "foobar" + Environment.NewLine }).SetName("TextToken Unspecified"); 27 | yield return new TestCaseData(new object[] { CreateTextToken("foobar", LineEnding.Unknown), "foobar" + Environment.NewLine }).SetName("TextToken Unknown"); 28 | yield return new TestCaseData(new object[] { CreateTextToken("foobar", LineEnding.None), "foobar"}).SetName("TextToken None"); 29 | yield return new TestCaseData(new object[] { CreateTextToken("foobar", LineEnding.Unix), "foobar\n" }).SetName("TextToken Unix"); 30 | yield return new TestCaseData(new object[] { CreateTextToken("foobar", LineEnding.Windows), "foobar\r\n" }).SetName("TextToken Windows"); 31 | yield return new TestCaseData(new object[] { CreateTextToken("foobar", LineEnding.Mac), "foobar\r" }).SetName("TextToken Mac"); 32 | } 33 | } 34 | 35 | public static IEnumerable Cases_Write_SetInstructionToken 36 | { 37 | get 38 | { 39 | InstructionType instructionType = InstructionType.Set; 40 | string tokenTypeName = "SetInstruction"; 41 | string expectedString = "myKey=myValue"; 42 | 43 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue"), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unspecified"); 44 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unknown), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unknown"); 45 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.None), expectedString }).SetName($"{tokenTypeName} None"); 46 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unix), $"{expectedString}\n" }).SetName($"{tokenTypeName} Unix"); 47 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Windows), $"{expectedString}\r\n" }).SetName($"{tokenTypeName} Windows"); 48 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Mac), $"{expectedString}\r" }).SetName($"{tokenTypeName} Mac"); 49 | } 50 | } 51 | 52 | public static IEnumerable Cases_Write_AddInstructionToken 53 | { 54 | get 55 | { 56 | InstructionType instructionType = InstructionType.Add; 57 | string tokenTypeName = "AddInstruction"; 58 | string expectedString = "+myKey=myValue"; 59 | 60 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue"), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unspecified"); 61 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unknown), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unknown"); 62 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.None), expectedString }).SetName($"{tokenTypeName} None"); 63 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unix), $"{expectedString}\n" }).SetName($"{tokenTypeName} Unix"); 64 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Windows), $"{expectedString}\r\n" }).SetName($"{tokenTypeName} Windows"); 65 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Mac), $"{expectedString}\r" }).SetName($"{tokenTypeName} Mac"); 66 | } 67 | } 68 | 69 | public static IEnumerable Cases_Write_AddForceInstructionToken 70 | { 71 | get 72 | { 73 | InstructionType instructionType = InstructionType.AddForce; 74 | string tokenTypeName = "AddForceInstruction"; 75 | string expectedString = ".myKey=myValue"; 76 | 77 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue"), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unspecified"); 78 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unknown), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unknown"); 79 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.None), expectedString }).SetName($"{tokenTypeName} None"); 80 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unix), $"{expectedString}\n" }).SetName($"{tokenTypeName} Unix"); 81 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Windows), $"{expectedString}\r\n" }).SetName($"{tokenTypeName} Windows"); 82 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Mac), $"{expectedString}\r" }).SetName($"{tokenTypeName} Mac"); 83 | } 84 | } 85 | 86 | public static IEnumerable Cases_Write_RemoveInstructionToken 87 | { 88 | get 89 | { 90 | InstructionType instructionType = InstructionType.Remove; 91 | string tokenTypeName = "RemoveInstruction"; 92 | string expectedString = "-myKey=myValue"; 93 | 94 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue"), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unspecified"); 95 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unknown), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unknown"); 96 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.None), expectedString }).SetName($"{tokenTypeName} None"); 97 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Unix), $"{expectedString}\n" }).SetName($"{tokenTypeName} Unix"); 98 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Windows), $"{expectedString}\r\n" }).SetName($"{tokenTypeName} Windows"); 99 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", "myValue", LineEnding.Mac), $"{expectedString}\r" }).SetName($"{tokenTypeName} Mac"); 100 | } 101 | } 102 | 103 | public static IEnumerable Cases_Write_RemoveAllInstructionToken 104 | { 105 | get 106 | { 107 | InstructionType instructionType = InstructionType.Remove; 108 | string tokenTypeName = "RemoveAllInstruction"; 109 | string expectedString = "-myKey"; 110 | 111 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey"), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unspecified"); 112 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", LineEnding.Unknown), $"{expectedString}{Environment.NewLine}" }).SetName($"{tokenTypeName} Unknown"); 113 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", LineEnding.None), expectedString }).SetName($"{tokenTypeName} None"); 114 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", LineEnding.Unix), $"{expectedString}\n" }).SetName($"{tokenTypeName} Unix"); 115 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", LineEnding.Windows), $"{expectedString}\r\n" }).SetName($"{tokenTypeName} Windows"); 116 | yield return new TestCaseData(new object[] { new InstructionToken(instructionType, "myKey", LineEnding.Mac), $"{expectedString}\r" }).SetName($"{tokenTypeName} Mac"); 117 | } 118 | } 119 | 120 | [TestCaseSource(nameof(Cases_Write_TextToken))] 121 | [TestCaseSource(nameof(Cases_Write_SetInstructionToken))] 122 | [TestCaseSource(nameof(Cases_Write_AddInstructionToken))] 123 | [TestCaseSource(nameof(Cases_Write_AddForceInstructionToken))] 124 | [TestCaseSource(nameof(Cases_Write_RemoveInstructionToken))] 125 | [TestCaseSource(nameof(Cases_Write_RemoveAllInstructionToken))] 126 | public void Write(LineToken token, string expectedText) 127 | { 128 | var writer = new ConfigIniWriter(new StringWriter()); 129 | token.Write(writer); 130 | Assert.That(writer.ToString(), Is.EqualTo(expectedText)); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/MultilineTokenTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using NUnit.Framework; 5 | using UE4Config.Parsing; 6 | 7 | namespace UE4Config.Tests.Parsing 8 | { 9 | [TestFixture] 10 | class MultilineTokenTests 11 | { 12 | [TestCase(typeof(WhitespaceToken))] 13 | [TestCase(typeof(CommentToken))] 14 | public void When_ConstructedDefault(Type tokenType) 15 | { 16 | var token = System.Activator.CreateInstance(tokenType, new object[] { }) as MultilineToken; 17 | Assert.That(token, Is.Not.Null); 18 | Assert.That(token.Lines, Is.Not.Null); 19 | Assert.That(token.Lines, Is.Empty); 20 | } 21 | 22 | [TestCase(typeof(WhitespaceToken), new[]{""})] 23 | [TestCase(typeof(WhitespaceToken), new[] { " ", "\t" })] 24 | [TestCase(typeof(CommentToken), new[]{"; Comment"})] 25 | [TestCase(typeof(CommentToken), new[] { ";Multi", " ;Line", "; Comment" })] 26 | public void When_ConstructedWithLines(Type tokenType, string[] lines) 27 | { 28 | var token = System.Activator.CreateInstance(tokenType, new object[] { lines, LineEnding.None }) as MultilineToken; 29 | Assert.That(token, Is.Not.Null); 30 | Assert.That(token.Lines, Is.Not.Null); 31 | Assert.That(token.GetStringLines(), Is.EquivalentTo(lines)); 32 | } 33 | 34 | [TestFixture] 35 | class Write 36 | { 37 | [TestCase(typeof(WhitespaceToken), new[] { "" }, LineEnding.Windows)] 38 | [TestCase(typeof(WhitespaceToken), new[] { " ", "\t" }, LineEnding.Windows)] 39 | [TestCase(typeof(CommentToken), new[] { "; Comment" }, LineEnding.Windows)] 40 | [TestCase(typeof(CommentToken), new[] { ";Multi", " ;Line", "; Comment" }, LineEnding.Windows)] 41 | public void When_UnspecifiedNewLine_UsesWriterNewLine(Type tokenType, string[] lines, LineEnding lineEnding) 42 | { 43 | var token = System.Activator.CreateInstance(tokenType, new object[] { lines, lineEnding }) as MultilineToken; 44 | var writer = new ConfigIniWriter(new StringWriter()); 45 | token.Write(writer); 46 | var expectedLines = String.Join(lineEnding.AsString(), lines); 47 | expectedLines += writer.ContentWriter.NewLine; //Expecting final newline 48 | 49 | Assert.That(writer.ToString(), Is.EqualTo(expectedLines)); 50 | } 51 | 52 | [TestCase(typeof(WhitespaceToken), new[] { " ", null, "\t" }, LineEnding.Windows)] 53 | [TestCase(typeof(CommentToken), new[] { ";Comment", null, "; WithNull" }, LineEnding.Windows)] 54 | public void When_HasNullLines_DoesSkipNulls(Type tokenType, string[] lines, LineEnding lineEnding) 55 | { 56 | var token = System.Activator.CreateInstance(tokenType, new object[] { lines, lineEnding }) as MultilineToken; 57 | var writer = new ConfigIniWriter(new StringWriter()); 58 | token.Write(writer); 59 | 60 | var linesWithoutNull = new List(lines); 61 | linesWithoutNull.RemoveAll((line) => line == null); 62 | var expectedLines = String.Join(lineEnding.AsString(), linesWithoutNull); 63 | expectedLines += writer.ContentWriter.NewLine; //Expecting final newline 64 | 65 | Assert.That(writer.ToString(), Is.EqualTo(expectedLines)); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /UE4Config.Tests/Parsing/WhitespaceTokenTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using UE4Config.Parsing; 3 | 4 | namespace UE4Config.Tests.Parsing 5 | { 6 | [TestFixture] 7 | class WhitespaceTokenTests 8 | { 9 | [TestFixture] 10 | class Condense 11 | { 12 | [Test] 13 | public void When_HasNoLines_DoesNotAdd() 14 | { 15 | var token = new WhitespaceToken(); 16 | 17 | Assert.That(() => token.Condense(), Throws.Nothing); 18 | 19 | Assert.That(token.Lines, Has.Count.EqualTo(0)); 20 | } 21 | 22 | [Test] 23 | public void When_HasOneLine() 24 | { 25 | var token = new WhitespaceToken(); 26 | token.Lines.Add(" "); 27 | 28 | Assert.That(() => token.Condense(), Throws.Nothing); 29 | 30 | Assert.That(token.Lines, Has.Count.EqualTo(1)); 31 | Assert.That(token.GetStringLines(), Is.EquivalentTo(new []{ string.Empty })); 32 | } 33 | 34 | [Test] 35 | public void When_HasMultipleLines() 36 | { 37 | var token = new WhitespaceToken(); 38 | token.Lines.Add(" "); 39 | token.Lines.Add(" "); 40 | token.Lines.Add(" \t "); 41 | 42 | Assert.That(() => token.Condense(), Throws.Nothing); 43 | 44 | Assert.That(token.Lines, Has.Count.EqualTo(1)); 45 | Assert.That(token.GetStringLines(), Is.EquivalentTo(new[] { string.Empty })); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UE4Config.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("UE4Config.Tests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("UE4Config.Tests")] 9 | [assembly: AssemblyCopyright("Copyright © Patrick Michael Hopf 2020")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("6001a205-97eb-4c5f-87f7-b91c4c41a6fc")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("1.0.0.0")] 19 | [assembly: AssemblyFileVersion("1.0.0.0")] 20 | -------------------------------------------------------------------------------- /UE4Config.Tests/SanityTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace UE4Config.Tests 4 | { 5 | [TestFixture] 6 | class SanityTests 7 | { 8 | [Test] 9 | public void When_CheckingTestSanity() 10 | { 11 | Assert.That("Sanity", Is.EqualTo("Sanity")); 12 | Assert.That(true, Is.True); 13 | Assert.That(false, Is.False); 14 | 15 | Assert.That(System.Convert.ToInt32(false), Is.EqualTo(0)); 16 | Assert.That(System.Convert.ToInt32(true), Is.EqualTo(1)); 17 | Assert.That(System.Convert.ToBoolean(0), Is.False); 18 | Assert.That(System.Convert.ToBoolean(1), Is.True); 19 | Assert.That(!!false, Is.False); 20 | Assert.That(!!true, Is.True); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Config/Base.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EngineBaseIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Config/BaseGame.ini: -------------------------------------------------------------------------------- 1 | [Internationalization] 2 | +LocalizationPaths=%GAMEDIR%Content/Localization/Game 3 | 4 | [MockSection] 5 | MockProperty=Value_EngineBaseGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Config/Switch/SwitchGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EngineSwitchGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Config/Windows/WindowsGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EngineWindowsGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Platforms/Linux/Config/LinuxGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EnginePlatformLinuxGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Platforms/Switch/Config/SwitchGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EnginePlatformSwitchGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockEngine/Platforms/XBoxOne/Config/XBoxOneGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_EnginePlatformXBoxOneGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Config/DefaultEditor.ini: -------------------------------------------------------------------------------- 1 | [EditoronlyBP] 2 | bAllowClassAndBlueprintPinMatching=true 3 | bReplaceBlueprintWithClass=true 4 | bDontLoadBlueprintOutsideEditor=true 5 | bBlueprintIsNotBlueprintType=true 6 | -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Config/DefaultEngine.ini: -------------------------------------------------------------------------------- 1 | [URL] 2 | GameName=StarterContent 3 | 4 | [/Script/Engine.RendererSettings] 5 | r.MobileHDR=True 6 | r.AllowOcclusionQueries=True 7 | r.MinScreenRadiusForLights=0.030000 8 | r.MinScreenRadiusForDepthPrepass=0.030000 9 | r.PrecomputedVisibilityWarning=False 10 | r.TextureStreaming=True 11 | Compat.UseDXT5NormalMaps=False 12 | r.AllowStaticLighting=True 13 | r.NormalMapsForStaticLighting=False 14 | r.GBuffer=True 15 | r.GenerateMeshDistanceFields=False 16 | r.Shadow.DistanceFieldPenumbraSize=0.050000 17 | r.TessellationAdaptivePixelsPerTriangle=48.000000 18 | r.SeparateTranslucency=True 19 | r.CustomDepth=1 20 | r.DefaultFeature.Bloom=True 21 | r.DefaultFeature.AmbientOcclusion=True 22 | r.DefaultFeature.AutoExposure=True 23 | r.DefaultFeature.MotionBlur=True 24 | r.DefaultFeature.LensFlare=True 25 | r.DefaultFeature.AntiAliasing=2 26 | r.EarlyZPass=3 27 | r.EarlyZPassMovable=False 28 | r.ClearSceneMethod=1 29 | r.MSAA.CompositingSampleCount=4 30 | r.WireframeCullThreshold=5.000000 31 | UIScaleCurve=(EditorCurveData=(Keys=((Time=480,Value=0.444),(Time=720,Value=0.666),(Time=1080,Value=1.0),(Time=8640,Value=8.0))),ExternalCurve=None) 32 | UIScaleCurve=(EditorCurveData=(Keys=((Time=480.000000,Value=0.444000),(Time=720.000000,Value=0.666000),(Time=1080.000000,Value=1.000000),(Time=8640.000000,Value=8.000000))),ExternalCurve=None) 33 | 34 | [/Script/EngineSettings.GameMapsSettings] 35 | EditorStartupMap=/Game/StarterContent/Maps/Minimal_Default 36 | LocalMapOptions= 37 | TransitionMap= 38 | bUseSplitscreen=True 39 | TwoPlayerSplitscreenLayout=Horizontal 40 | ThreePlayerSplitscreenLayout=FavorTop 41 | GameInstanceClass=/Script/Engine.GameInstance 42 | GameDefaultMap=/Game/StarterContent/Maps/Minimal_Default 43 | ServerDefaultMap=/Engine/Maps/Entry 44 | GlobalDefaultGameMode=/Script/Engine.GameMode 45 | GlobalDefaultServerGameMode=None 46 | 47 | [/Script/IOSRuntimeSettings.IOSRuntimeSettings] 48 | MinimumiOSVersion=IOS_11 49 | -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Config/DefaultGame.ini: -------------------------------------------------------------------------------- 1 | [/Script/EngineSettings.GeneralProjectSettings] 2 | ProjectID=3F9D696D4363312194B0ECB2671E899F 3 | 4 | [Internationalization] 5 | +LocalizationPaths=%GAMEDIR%Content/Localization/Game/Default 6 | 7 | [MockSection] 8 | MockProperty=Value_DefaultGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Config/Switch/SwitchGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_SwitchGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Config/Windows/WindowsGame.ini: -------------------------------------------------------------------------------- 1 | [Internationalization] 2 | +LocalizationPaths=%GAMEDIR%Content/Localization/Game/Win64 3 | 4 | [Global] 5 | GUIDs=a44b 6 | 7 | OtherProp=otherVal 8 | 9 | 10 | 11 | [MockSection] 12 | MockProperty=Value_WindowsGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Platforms/Mac/Config/MacGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_ProjectPlatformMacGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Platforms/Switch/Config/SwitchGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_PlatformSwitchGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestData/MockProject/Platforms/XBoxOne/Config/XBoxOneGame.ini: -------------------------------------------------------------------------------- 1 | [MockSection] 2 | MockProperty=Value_PlatformXBoxOneGameIni -------------------------------------------------------------------------------- /UE4Config.Tests/TestUtils.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | 4 | namespace UE4Config.Tests 5 | { 6 | public class TestUtils 7 | { 8 | public static string GetTestDataPath() 9 | { 10 | return Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData"); 11 | } 12 | 13 | public static string GetTestDataPath(string path) 14 | { 15 | return Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", path); 16 | } 17 | 18 | public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) 19 | { 20 | // Get information about the source directory 21 | var dir = new DirectoryInfo(sourceDir); 22 | 23 | // Check if the source directory exists 24 | if (!dir.Exists) 25 | throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); 26 | 27 | // Cache directories before we start copying 28 | DirectoryInfo[] dirs = dir.GetDirectories(); 29 | 30 | // Create the destination directory 31 | Directory.CreateDirectory(destinationDir); 32 | 33 | // Get the files in the source directory and copy to the destination directory 34 | foreach (FileInfo file in dir.GetFiles()) 35 | { 36 | string targetFilePath = Path.Combine(destinationDir, file.Name); 37 | file.CopyTo(targetFilePath); 38 | } 39 | 40 | // If recursive and copying subdirectories, recursively call this method 41 | if (recursive) 42 | { 43 | foreach (DirectoryInfo subDir in dirs) 44 | { 45 | string newDestinationDir = Path.Combine(destinationDir, subDir.Name); 46 | CopyDirectory(subDir.FullName, newDestinationDir, true); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /UE4Config.Tests/UE4Config.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Debug 8 | AnyCPU 9 | {6001A205-97EB-4C5F-87F7-B91C4C41A6FC} 10 | Library 11 | Properties 12 | UE4Config.Tests 13 | UE4Config.Tests 14 | v4.7.2 15 | 512 16 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | 15.0 18 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 19 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 20 | False 21 | UnitTest 22 | 23 | 24 | 25 | 26 | true 27 | full 28 | false 29 | bin\Debug\ 30 | DEBUG;TRACE 31 | prompt 32 | 4 33 | 34 | 35 | pdbonly 36 | true 37 | bin\Release\ 38 | TRACE 39 | prompt 40 | 4 41 | 42 | 43 | 44 | ..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Always 84 | 85 | 86 | Always 87 | 88 | 89 | Always 90 | 91 | 92 | Always 93 | 94 | 95 | Always 96 | 97 | 98 | Always 99 | 100 | 101 | Always 102 | 103 | 104 | Always 105 | 106 | 107 | Always 108 | 109 | 110 | 111 | 112 | {b84d5893-3be7-4bab-a1f5-480806eb66f2} 113 | UE4Config 114 | 115 | 116 | 117 | 118 | Always 119 | 120 | 121 | Always 122 | 123 | 124 | Always 125 | 126 | 127 | Always 128 | 129 | 130 | Always 131 | 132 | 133 | Always 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /UE4Config.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /UE4Config.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UE4Config", "UE4Config\UE4Config.csproj", "{B84D5893-3BE7-4BAB-A1F5-480806EB66F2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UE4Config.Tests", "UE4Config.Tests\UE4Config.Tests.csproj", "{6001A205-97EB-4C5F-87F7-B91C4C41A6FC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B84D5893-3BE7-4BAB-A1F5-480806EB66F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B84D5893-3BE7-4BAB-A1F5-480806EB66F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B84D5893-3BE7-4BAB-A1F5-480806EB66F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B84D5893-3BE7-4BAB-A1F5-480806EB66F2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {6001A205-97EB-4C5F-87F7-B91C4C41A6FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6001A205-97EB-4C5F-87F7-B91C4C41A6FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6001A205-97EB-4C5F-87F7-B91C4C41A6FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {6001A205-97EB-4C5F-87F7-B91C4C41A6FC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {7EEE1DB9-9A14-4C9F-8CF4-6194F08F1480} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /UE4Config/Evaluation/PropertyEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | using UE4Config.Parsing; 4 | 5 | namespace UE4Config.Evaluation 6 | { 7 | public class PropertyEvaluator 8 | { 9 | /// 10 | /// Returns a default already instanced PropertyEvaluater for easy use. 11 | /// Instances a new default if there was none set yet. 12 | /// 13 | public static PropertyEvaluator Default 14 | { 15 | get 16 | { 17 | if (m_Default == null) 18 | { 19 | m_Default = new PropertyEvaluator(); 20 | } 21 | return m_Default; 22 | } 23 | } 24 | 25 | /// 26 | /// Sets the instance to eb returned by 27 | /// 28 | public static void SetDefaultEvaluator(PropertyEvaluator evaluator) 29 | { 30 | m_Default = evaluator; 31 | } 32 | 33 | public static void ResetDefaultEvaluator() 34 | { 35 | m_Default = null; 36 | } 37 | 38 | private static PropertyEvaluator m_Default; 39 | 40 | public static PropertyEvaluator CustomOrDefault(PropertyEvaluator customPick = null) 41 | { 42 | return customPick != null ? customPick : Default; 43 | } 44 | 45 | /// 46 | /// Executes ordered list of instructions, assuming they are for the same property, modifying its in the progress. 47 | /// 48 | /// 49 | /// Queue of instructions to be executed 50 | /// The values of the property the instructions will be executed for. Will be modified by execution. 51 | public void ExecutePropertyInstructions(IList instructions, IList propertyValues) 52 | { 53 | foreach (var instruction in instructions) 54 | { 55 | if (instruction != null) 56 | { 57 | ExecutePropertyInstruction(instruction, propertyValues); 58 | } 59 | } 60 | } 61 | 62 | /// 63 | /// Executes a single instruction, modifying the of a property. 64 | /// 65 | /// The instruction to be executed 66 | /// The values of the property the instruction will be executed for. Will be modified by execution. 67 | public void ExecutePropertyInstruction(InstructionToken instruction, IList propertyValues) 68 | { 69 | switch (instruction.InstructionType) 70 | { 71 | case InstructionType.Add: 72 | if (!propertyValues.Contains(instruction.Value)) 73 | { 74 | propertyValues.Add(instruction.Value); 75 | } 76 | break; 77 | case InstructionType.AddForce: 78 | propertyValues.Add(instruction.Value); 79 | break; 80 | case InstructionType.Remove: 81 | bool wasRemoved = false; 82 | do 83 | { 84 | wasRemoved = propertyValues.Remove(instruction.Value); 85 | } while (wasRemoved); 86 | break; 87 | case InstructionType.RemoveAll: 88 | propertyValues.Clear(); 89 | break; 90 | case InstructionType.Set: 91 | propertyValues.Clear(); 92 | propertyValues.Add(instruction.Value); 93 | break; 94 | default: 95 | throw new InvalidEnumArgumentException(nameof(instruction.InstructionType), (int)instruction.InstructionType, typeof(InstructionType)); 96 | } 97 | } 98 | 99 | /// 100 | /// Evaluates the value of an property, as constructed by one or more s 101 | /// 102 | /// Ordered list of configs, with the latest / highest priority declarations being last 103 | /// The section the property should be fetched from 104 | /// The property key (name) to fetch 105 | /// Cache for the list of instructions regarding this property. Will be added in order of declaration, the latest one being last 106 | /// Resulting list of values, possibly empty if the property was deleted 107 | public void EvaluatePropertyValues(IEnumerable configs, string sectionName, string propertyKey, 108 | IList instructions, IList values) 109 | { 110 | foreach (var configIni in configs) 111 | { 112 | configIni.FindPropertyInstructions(sectionName, propertyKey, instructions); 113 | } 114 | ExecutePropertyInstructions(instructions, values); 115 | } 116 | 117 | public void EvaluatePropertyValues(IEnumerable configs, string sectionName, string propertyKey, IList values) 118 | { 119 | EvaluatePropertyValues(configs, sectionName, propertyKey, new List(), values); 120 | } 121 | 122 | public void EvaluatePropertyValues(ConfigIni config, string sectionName, string propertyKey, 123 | IList instructions, IList values) 124 | { 125 | config.FindPropertyInstructions(sectionName, propertyKey, instructions); 126 | ExecutePropertyInstructions(instructions, values); 127 | } 128 | 129 | public void EvaluatePropertyValues(ConfigIni configs, string sectionName, string propertyKey, IList values) 130 | { 131 | EvaluatePropertyValues(configs, sectionName, propertyKey, new List(), values); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigBranchExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UE4Config.Parsing; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | public enum ConfigBranchPlatformSelector 7 | { 8 | NoneOrAny, 9 | None, 10 | Any, 11 | Specific 12 | } 13 | 14 | public static class ConfigBranchExtensions 15 | { 16 | /// 17 | /// Utility method to select the last config file of the branch that is still in the given domain and given platform 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// The most prioritized config file in the , or an invalid 25 | /// 26 | public static ConfigFileReference SelectHeadConfig(this IReadOnlyList configBranch, ConfigDomain configDomain, ConfigBranchPlatformSelector platformSelector = ConfigBranchPlatformSelector.NoneOrAny, string specifcPlatformIdentifier = null) 27 | { 28 | var index = FindHeadConfigIndex(configBranch, configDomain, platformSelector, specifcPlatformIdentifier); 29 | if (index >= 0) 30 | { 31 | return configBranch[index]; 32 | } 33 | 34 | return default(ConfigFileReference); 35 | } 36 | 37 | public static ConfigIni SelectHeadConfig(this IReadOnlyList configBranch, ConfigDomain configDomain, ConfigBranchPlatformSelector platformSelector = ConfigBranchPlatformSelector.NoneOrAny, string specifcPlatformIdentifier = null) 38 | { 39 | var index = FindHeadConfigIndex(configBranch, configDomain, platformSelector, specifcPlatformIdentifier); 40 | if (index >= 0) 41 | { 42 | return configBranch[index]; 43 | } 44 | 45 | return default(ConfigIni); 46 | } 47 | 48 | private static int FindHeadConfigIndex(this IReadOnlyList configBranch, ConfigDomain configDomain, ConfigBranchPlatformSelector platformSelector, string specifcPlatformIdentifier) 49 | { 50 | for (int i = configBranch.Count - 1; i >= 0; i--) 51 | { 52 | var pivotRef = configBranch[i]; 53 | if (IsHeadConfigReference(pivotRef, configDomain, platformSelector, specifcPlatformIdentifier)) 54 | { 55 | return i; 56 | } 57 | } 58 | 59 | return -1; 60 | } 61 | 62 | private static int FindHeadConfigIndex(this IReadOnlyList configBranch, ConfigDomain configDomain, ConfigBranchPlatformSelector platformSelector, string specifcPlatformIdentifier) 63 | { 64 | for (int i = configBranch.Count - 1; i >= 0; i--) 65 | { 66 | var pivot = configBranch[i]; 67 | var pivotRef = pivot.Reference; 68 | if (IsHeadConfigReference(pivotRef, configDomain, platformSelector, specifcPlatformIdentifier)) 69 | { 70 | return i; 71 | } 72 | } 73 | 74 | return -1; 75 | } 76 | 77 | private static bool IsHeadConfigReference(ConfigFileReference reference, ConfigDomain configDomain, ConfigBranchPlatformSelector platformSelector, string specifcPlatformIdentifier) 78 | { 79 | if (reference.Domain != configDomain) 80 | { 81 | return false; 82 | } 83 | switch (platformSelector) 84 | { 85 | case ConfigBranchPlatformSelector.None: 86 | if (reference.IsPlatformConfig) 87 | { 88 | return false; 89 | } 90 | break; 91 | case ConfigBranchPlatformSelector.Any: 92 | if (!reference.IsPlatformConfig) 93 | { 94 | return false; 95 | } 96 | break; 97 | case ConfigBranchPlatformSelector.Specific: 98 | if (reference.Platform == null || reference.Platform?.Identifier != specifcPlatformIdentifier) 99 | { 100 | return false; 101 | } 102 | break; 103 | case ConfigBranchPlatformSelector.NoneOrAny: 104 | default: 105 | break; 106 | } 107 | 108 | return true; 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigCategory.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Hierarchy 2 | { 3 | public static class ConfigCategory 4 | { 5 | public const string Compat = "Compat"; 6 | public const string DeviceProfiles = "DeviceProfiles"; 7 | public const string Editor = "Editor"; 8 | public const string EditorGameAgnostic = "EditorGameAgnostic"; 9 | public const string EditorKeyBindings = "EditorKeyBindings"; 10 | public const string EditorUserSettings = "EditorUserSettings"; 11 | public const string Engine = "Engine"; 12 | public const string Game = "Game"; 13 | public const string Input = "Input"; 14 | public const string Lightmass = "Lightmass"; 15 | public const string Scalability = "Scalability"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigDomain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | /// 6 | /// The configuration domain identifies where a configuration is stored and modified. 7 | /// 8 | [Flags] 9 | public enum ConfigDomain 10 | { 11 | None = 0b00000000, 12 | /// 13 | /// Engine/Base.ini 14 | /// "{ENGINE}/Config/Base.ini" 15 | /// 16 | /// 17 | /// Engine/Base*.ini 18 | /// "{ENGINE}/Config/Base{TYPE}.ini" 19 | /// 20 | /// 21 | /// Engine/Platform/BasePlatform*.ini 22 | /// "{ENGINE}/Config/{PLATFORM}/Base{PLATFORM}{TYPE}.ini" 23 | /// 24 | EngineBase = 0b00000010, 25 | /// 26 | /// Engine/Platform/Platform*.ini 27 | /// "{ENGINE}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini" 28 | /// 29 | Engine = 0b00000100, 30 | /// 31 | /// Project/Default*.ini 32 | /// "{PROJECT}/Config/Default{TYPE}.ini" 33 | /// 34 | /// 35 | /// Project/Default*.ini 36 | /// "{PROJECT}/Config/Default{TYPE}.ini" 37 | /// 38 | /// 39 | /// Project/Platform/Platform*.ini 40 | /// "{PROJECT}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini" 41 | /// 42 | Project = 0b00001000, 43 | /// 44 | /// Project config files which are generated by buildmachine processes (i.e. should never be checked in) 45 | /// 46 | /// 47 | /// Project/Platform/Generated*.ini 48 | /// {PROJECT}/Config/Generated{TYPE}.ini 49 | /// 50 | /// 51 | /// Project/Platform/GeneratedPlatform*.ini 52 | /// {PROJECT}/Config/{PLATFORM}/Generated{PLATFORM}{TYPE}.ini 53 | /// 54 | ProjectGenerated = 0b00010000, 55 | /* 56 | TODO Support user level layers 57 | /// 58 | /// {USERSETTINGS}/Unreal Engine/Engine/Config/User{TYPE}.ini 59 | /// UserSettings/.../User*.ini 60 | /// 61 | UserSettings, 62 | /// 63 | /// Global user settings 64 | /// 65 | /// 66 | /// {USER}/Unreal Engine/Engine/Config/User{TYPE}.ini 67 | /// UserDir/.../User*.ini 68 | /// 69 | GlobalUser, 70 | /// 71 | /// User project overrides 72 | /// 73 | /// 74 | /// {PROJECT}/User{TYPE}.ini 75 | /// Project/User*.ini 76 | /// 77 | ProjectUser, 78 | */ 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigFileIOAdapter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | /// 7 | /// Default implementation of the 8 | /// 9 | public class ConfigFileIOAdapter : IConfigFileIOAdapter 10 | { 11 | public List GetDirectories(string pivotPath) 12 | { 13 | return new List(Directory.GetDirectories(pivotPath)); 14 | } 15 | 16 | public StreamReader OpenRead(string filePath) 17 | { 18 | return File.OpenText(filePath); 19 | } 20 | 21 | public StreamWriter OpenWrite(string filePath) 22 | { 23 | FileStream fileStream; 24 | fileStream = File.OpenWrite(filePath); 25 | return new StreamWriter(fileStream); 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigFileReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | /// 6 | /// Represents a single config file that may or may not exist yet. 7 | /// Needs to be interpreted and provided by a ConfigFileProvider. 8 | /// 9 | public struct ConfigFileReference 10 | { 11 | public static ConfigFileReference None => new ConfigFileReference(); 12 | public ConfigDomain Domain { get; private set; } 13 | public IConfigPlatform Platform { get; private set; } 14 | public string Type { get; private set; } 15 | 16 | public bool IsPlatformConfig => Platform != null; 17 | 18 | public ConfigFileReference(ConfigDomain domain, IConfigPlatform platform, string type) 19 | { 20 | if (type != null && string.IsNullOrWhiteSpace(type)) 21 | { 22 | throw new ArgumentException(nameof(type), "Argument cannot be an empty or whitespace string"); 23 | } else if (type?.ToLowerInvariant() == "default") 24 | { 25 | throw new ArgumentException(nameof(type), "Argument cannot be \"Default\""); 26 | } else if (type?.ToLowerInvariant() == "base") 27 | { 28 | throw new ArgumentException(nameof(type), "Argument cannot be \"Base\""); 29 | } 30 | Domain = domain; 31 | Platform = platform; 32 | Type = type; 33 | } 34 | 35 | public override string ToString() 36 | { 37 | var result = nameof(ConfigFileReference); 38 | result += ":"+Domain; 39 | if (Platform != null) 40 | { 41 | result += "@" + Platform.Identifier; 42 | } 43 | 44 | if (Type != null) 45 | { 46 | result += ":" + Type; 47 | } 48 | 49 | return result; 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigHierarchy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UE4Config.Evaluation; 4 | using UE4Config.Parsing; 5 | 6 | namespace UE4Config.Hierarchy 7 | { 8 | /// 9 | /// STUB 10 | /// 11 | [Obsolete] 12 | public abstract class ConfigHierarchy 13 | { 14 | /// 15 | /// The default platform identifier - used when no specific platform is targeted 16 | /// 17 | public const string DefaultPlatform = "Default"; 18 | 19 | /// 20 | /// Returns if there is a "platform extension" folder structure for the given platform in the engine directory. 21 | /// This structure is being introduced since 4.24 22 | /// If true, will use different paths for engine platform config files. 23 | /// 24 | public abstract bool CheckEngineHasPlatformExtension(string platform); 25 | 26 | /// 27 | /// Returns if there is a "platform extension" folder structure for the given platform in the engine directory. 28 | /// This structure is being introduced since 4.24 29 | /// If true, will use different paths for project platform config files. 30 | /// 31 | public abstract bool CheckProjectHasPlatformExtension(string platform); 32 | 33 | /// 34 | /// Returns the platform config of the given and the given , if available. 35 | /// Returns null otherwise. 36 | /// 37 | public abstract ConfigIni GetConfig(string platform, string category, ConfigHierarchyLevel level); 38 | 39 | /// 40 | /// Returns the default config of the given and the given , if available. 41 | /// Returns null otherwise. 42 | /// 43 | /// 44 | public ConfigIni GetConfig(string category, ConfigHierarchyLevel level) 45 | { 46 | return GetConfig(DefaultPlatform, category, level); 47 | } 48 | 49 | /// 50 | /// Gets the configs for the given platform & category in order of ascending levels ( being the first) 51 | /// 52 | public void GetConfigs(string platform, string category, ConfigHierarchyLevelRange range, IList configs) 53 | { 54 | var levels = ConfigHierarchyLevelExtensions.GetLevelsAscending(); 55 | foreach (var level in levels) 56 | { 57 | if (range.Includes(level)) 58 | { 59 | ConfigIni config = GetConfig(platform, category, level); 60 | if (config != null) 61 | { 62 | configs.Add(config); 63 | } 64 | } 65 | } 66 | } 67 | 68 | /// 69 | public void GetConfigs(string platform, string category, IList configs) 70 | { 71 | GetConfigs(platform, category, ConfigHierarchyLevelRange.All(), configs); 72 | } 73 | 74 | /// 75 | /// Gets the default configs for the category in order of ascending levels ( being the first) 76 | /// 77 | public void GetConfigs(string category, ConfigHierarchyLevelRange range, IList configs) 78 | { 79 | GetConfigs(DefaultPlatform, category, range, configs); 80 | } 81 | 82 | public void GetConfigs(string category, IList configs) 83 | { 84 | GetConfigs(category, ConfigHierarchyLevelRange.All(), configs); 85 | } 86 | 87 | /// 88 | /// Publishes a new config or replaces a previous one as the factual one 89 | /// 90 | public abstract void PublishConfig(string platform, string category, ConfigHierarchyLevel level, ConfigIni config); 91 | 92 | /// 93 | /// Publishes a new config or replaces a previous one as the factual one for the default platform 94 | /// 95 | /// 96 | public void PublishConfig(string category, ConfigHierarchyLevel level, ConfigIni config) 97 | { 98 | PublishConfig(DefaultPlatform, category, level, config); 99 | } 100 | 101 | /// 102 | /// Creates the platform config of the given and the given . 103 | /// Will not automatically save/write the config, so it needs to be saved before the next Get can be sure to return it. 104 | /// 105 | public abstract ConfigIni CreateConfig(string platform, string category, ConfigHierarchyLevel level); 106 | 107 | /// 108 | /// Gets the platform config of the given and the given , creating it 109 | /// if it doesn't exist yet. 110 | /// 111 | /// 112 | /// 113 | public ConfigIni GetOrCreateConfig(string platform, string category, ConfigHierarchyLevel level, out bool wasCreated) 114 | { 115 | ConfigIni config = GetConfig(platform, category, level); 116 | if (config == null) 117 | { 118 | config = CreateConfig(platform, category, level); 119 | wasCreated = true; 120 | } 121 | else 122 | { 123 | wasCreated = false; 124 | } 125 | return config; 126 | } 127 | 128 | /// 129 | /// Returns the default config of the given and the given , if available. 130 | /// Returns null otherwise. 131 | /// 132 | /// 133 | public ConfigIni GetOrCreateConfig(string category, ConfigHierarchyLevel level, out bool wasCreated) 134 | { 135 | return GetOrCreateConfig(DefaultPlatform, category, level, out wasCreated); 136 | } 137 | 138 | /// 139 | /// Evaluates a properties values over this hierarchy of configs. 140 | /// 141 | public virtual void EvaluatePropertyValues(string platform, string category, string sectionName, string propertyKey, ConfigHierarchyLevelRange range, 142 | PropertyEvaluator evaluator, IList values) 143 | { 144 | List configs = new List(); 145 | GetConfigs(platform, category, range, configs); 146 | evaluator = PropertyEvaluator.CustomOrDefault(evaluator); 147 | evaluator.EvaluatePropertyValues(configs, sectionName, propertyKey, values); 148 | } 149 | 150 | /// 151 | public void EvaluatePropertyValues(string platform, string category, string sectionName, string propertyKey, PropertyEvaluator evaluator, IList values) 152 | { 153 | EvaluatePropertyValues(platform, category, sectionName, propertyKey, ConfigHierarchyLevelRange.All(), 154 | evaluator, values); 155 | } 156 | 157 | /// 158 | /// 159 | /// Uses as evaluator 160 | /// 161 | public void EvaluatePropertyValues(string platform, string category, string sectionName, string propertyKey, ConfigHierarchyLevelRange range, IList values) 162 | { 163 | EvaluatePropertyValues(platform, category, sectionName, propertyKey, range, PropertyEvaluator.Default, values); 164 | } 165 | 166 | /// 167 | /// 168 | /// Uses as evaluator 169 | /// 170 | public void EvaluatePropertyValues(string platform, string category, string sectionName, string propertyKey, IList values) 171 | { 172 | EvaluatePropertyValues(platform, category, sectionName, propertyKey, ConfigHierarchyLevelRange.All(), values); 173 | } 174 | 175 | /// 176 | /// 177 | /// Uses "Default" platform 178 | /// Uses as evaluator 179 | /// 180 | public void EvaluatePropertyValues(string category, string sectionName, string propertyKey, ConfigHierarchyLevelRange range, IList values) 181 | { 182 | EvaluatePropertyValues(DefaultPlatform, category, sectionName, propertyKey, range, PropertyEvaluator.Default, values); 183 | } 184 | 185 | /// 186 | /// 187 | /// Uses "Default" platform 188 | /// Uses as evaluator 189 | /// 190 | public void EvaluatePropertyValues(string category, string sectionName, string propertyKey, IList values) 191 | { 192 | EvaluatePropertyValues(category, sectionName, propertyKey, ConfigHierarchyLevelRange.All(), values); 193 | } 194 | 195 | /// 196 | /// 197 | /// Uses "Default" platform 198 | /// 199 | public void EvaluatePropertyValues(string category, string sectionName, string propertyKey, ConfigHierarchyLevelRange range, PropertyEvaluator evaluator, IList values) 200 | { 201 | EvaluatePropertyValues(DefaultPlatform, category, sectionName, propertyKey, range, evaluator, values); 202 | } 203 | 204 | /// 205 | /// 206 | /// Uses "Default" platform 207 | /// 208 | public void EvaluatePropertyValues(string category, string sectionName, string propertyKey, PropertyEvaluator evaluator, IList values) 209 | { 210 | EvaluatePropertyValues(category, sectionName, propertyKey, ConfigHierarchyLevelRange.All(), evaluator, values); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigHierarchyLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | /// 6 | /// Represents a level of the Unreal Engine 4 configuration hierarchy 7 | /// 8 | [Obsolete] 9 | public enum ConfigHierarchyLevel 10 | { 11 | /// 12 | /// Engine/Config/Base.ini 13 | /// 14 | Base, 15 | /// 16 | /// Engine/Config/Base[Category].ini 17 | /// 18 | /// 19 | /// Engine/Config/BaseEngine.ini 20 | /// 21 | BaseCategory, 22 | /// 23 | /// Engine/Config/[Platform]/[Platform][Category].ini 24 | /// 25 | /// 26 | /// Engine/Config/PS4/PS4Engine.ini 27 | /// 28 | BasePlatformCategory, 29 | /// 30 | /// [ProjectDirectory]/Config/Default[Category].ini 31 | /// 32 | /// 33 | /// [ProjectDirectory]/Config/DefaultEngine.ini 34 | /// 35 | ProjectCategory, 36 | /// 37 | /// [ProjectDirectory]/Config/[Platform]/[Platform][Category].ini 38 | /// 39 | /// 40 | /// [ProjectDirectory]/Config/PS4/PS4Engine.ini 41 | /// 42 | ProjectPlatformCategory 43 | } 44 | 45 | public static class ConfigHierarchyLevelExtensions 46 | { 47 | /// 48 | /// Returns a from this level to a specific other level 49 | /// 50 | /// 51 | public static ConfigHierarchyLevelRange To(this ConfigHierarchyLevel from, ConfigHierarchyLevel to) 52 | { 53 | return ConfigHierarchyLevelRange.FromTo(from, to); 54 | } 55 | 56 | /// 57 | /// Returns a from this level to any lower 58 | /// 59 | /// 60 | public static ConfigHierarchyLevelRange AndLower(this ConfigHierarchyLevel level) 61 | { 62 | return ConfigHierarchyLevelRange.AnyTo(level); 63 | } 64 | 65 | /// 66 | /// Returns a from this level to any higher 67 | /// 68 | /// 69 | public static ConfigHierarchyLevelRange AndHigher(this ConfigHierarchyLevel level) 70 | { 71 | return ConfigHierarchyLevelRange.AnyFrom(level); 72 | } 73 | 74 | /// 75 | /// Returns a exactly representing this level. 76 | /// 77 | /// 78 | public static ConfigHierarchyLevelRange Exact(this ConfigHierarchyLevel level) 79 | { 80 | return ConfigHierarchyLevelRange.Exact(level); 81 | } 82 | 83 | /// 84 | /// Returns the levels in ascending order ( being the first) 85 | /// 86 | public static ConfigHierarchyLevel[] GetLevelsAscending() 87 | { 88 | if (m_LevelsAscending == null) 89 | { 90 | m_LevelsAscending = (ConfigHierarchyLevel[])Enum.GetValues(typeof(ConfigHierarchyLevel)); 91 | } 92 | 93 | return m_LevelsAscending; 94 | } 95 | 96 | private static ConfigHierarchyLevel[] m_LevelsAscending; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigHierarchyLevelRange.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Hierarchy 2 | { 3 | /// 4 | /// Represents a range of s 5 | /// 6 | public struct ConfigHierarchyLevelRange 7 | { 8 | 9 | /// 10 | /// True if represents a bound to be respected. False is there is no lower bound. 11 | /// 12 | public bool HasFrom => m_HasFrom; 13 | 14 | /// 15 | /// The lowest still included in the range 16 | /// 17 | public ConfigHierarchyLevel From { get; private set; } 18 | 19 | /// 20 | /// The topmost still included in the range 21 | /// 22 | public ConfigHierarchyLevel To { get; private set; } 23 | 24 | /// 25 | /// True if represents a bound to be respected. False is there is no upper bound. 26 | /// 27 | public bool HasTo => m_HasTo; 28 | 29 | /// 30 | /// True if this range includes any level at all. If false, no level will match this range. 31 | /// 32 | public bool IncludesAnything => m_IncludesAnything; 33 | 34 | public ConfigHierarchyLevelRange(ConfigHierarchyLevel from, ConfigHierarchyLevel to) 35 | { 36 | m_IncludesAnything = true; 37 | m_HasFrom = true; 38 | m_HasTo = true; 39 | if (from <= to) 40 | { 41 | From = from; 42 | To = to; 43 | } 44 | else 45 | { 46 | To = from; 47 | From = to; 48 | } 49 | } 50 | 51 | public bool Includes(ConfigHierarchyLevel level) 52 | { 53 | if (!IncludesAnything) 54 | return false; 55 | if (HasFrom && level < From) 56 | return false; 57 | if (HasTo && level > To) 58 | return false; 59 | return true; 60 | } 61 | 62 | public static ConfigHierarchyLevelRange All() 63 | { 64 | var range = new ConfigHierarchyLevelRange(); 65 | range.m_IncludesAnything = true; 66 | return range; 67 | } 68 | 69 | public static ConfigHierarchyLevelRange None() 70 | { 71 | return new ConfigHierarchyLevelRange(); 72 | } 73 | 74 | public static ConfigHierarchyLevelRange AnyFrom(ConfigHierarchyLevel from) 75 | { 76 | var range = new ConfigHierarchyLevelRange(from, from); 77 | range.m_HasTo = false; 78 | return range; 79 | } 80 | 81 | public static ConfigHierarchyLevelRange AnyTo(ConfigHierarchyLevel to) 82 | { 83 | var range = new ConfigHierarchyLevelRange(to, to); 84 | range.m_HasFrom = false; 85 | return range; 86 | } 87 | 88 | public static ConfigHierarchyLevelRange FromTo(ConfigHierarchyLevel from, ConfigHierarchyLevel to) 89 | { 90 | return new ConfigHierarchyLevelRange(from, to); 91 | } 92 | 93 | public static ConfigHierarchyLevelRange Exact(ConfigHierarchyLevel level) 94 | { 95 | return new ConfigHierarchyLevelRange(level, level); 96 | } 97 | 98 | private bool m_IncludesAnything; 99 | private bool m_HasFrom; 100 | private bool m_HasTo; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigPlatform.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Hierarchy 2 | { 3 | /// 4 | /// Default implementation of an 5 | /// 6 | public class ConfigPlatform : IConfigPlatform 7 | { 8 | public string Identifier { get; } 9 | public IConfigPlatform ParentPlatform { get; } 10 | public ConfigPlatform(string identifier, IConfigPlatform parent = null) 11 | { 12 | Identifier = identifier; 13 | ParentPlatform = parent; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/ConfigReferenceTree427.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | /// 7 | /// The configuration tree provides a common interface to generate config chains that represent 8 | /// config types for specific target platforms. 9 | /// 10 | /// 11 | /// This replaces the ConfigHierarchy used in older versions of this codebase 12 | /// This does not support layer-hierarchies below UE427+ 13 | /// This does not yet support user-layers 14 | /// 15 | public class ConfigReferenceTree427 : IConfigReferenceTree 16 | { 17 | public IConfigFileProvider FileProvider { get; private set; } 18 | 19 | public void Setup(IConfigFileProvider configFileProvider) 20 | { 21 | FileProvider = configFileProvider; 22 | } 23 | 24 | public void VisitConfigRoot(Action onConfig) 25 | { 26 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.EngineBase, null, null)); 27 | } 28 | 29 | public void VisitConfigBranch(string configType, string platformIdentifier, Action onConfig) 30 | { 31 | VisitConfigRoot(onConfig); 32 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.EngineBase, null, configType)); 33 | var targetPlatform = GetOrCreatePlatform(platformIdentifier); 34 | var platforms = new List(); 35 | targetPlatform?.ResolvePlatformInheritance(ref platforms); 36 | foreach (var platform in platforms) 37 | { 38 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.EngineBase, platform, configType)); 39 | } 40 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.Project, null, configType)); 41 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.ProjectGenerated, null, configType)); 42 | foreach (var platform in platforms) 43 | { 44 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.Engine, platform, configType)); 45 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.Project, platform, configType)); 46 | onConfig?.Invoke(GetOrCreateConfigFileReference(ConfigDomain.ProjectGenerated, platform, configType)); 47 | } 48 | } 49 | 50 | public IConfigPlatform GetPlatform(string platformIdentifier) 51 | { 52 | if (m_Platforms.ContainsKey(platformIdentifier)) 53 | { 54 | return m_Platforms[platformIdentifier]; 55 | } 56 | 57 | return null; 58 | } 59 | 60 | public IConfigPlatform RegisterPlatform(string platformIdentifier, IConfigPlatform parentPlatform = null) 61 | { 62 | var platform = new ConfigPlatform(platformIdentifier, parentPlatform); 63 | m_Platforms.Add(platformIdentifier, platform); 64 | return platform; 65 | } 66 | 67 | private IConfigPlatform GetOrCreatePlatform(string platformIdentifier) 68 | { 69 | if (String.IsNullOrEmpty(platformIdentifier)) 70 | { 71 | return null; 72 | } 73 | 74 | if (!m_Platforms.ContainsKey(platformIdentifier)) 75 | { 76 | m_Platforms.Add(platformIdentifier, new ConfigPlatform(platformIdentifier)); 77 | } 78 | 79 | return m_Platforms[platformIdentifier]; 80 | } 81 | 82 | private ConfigFileReference GetOrCreateConfigFileReference(ConfigDomain domain, IConfigPlatform platform, 83 | string type) 84 | { 85 | return new ConfigFileReference(domain, platform, type); 86 | } 87 | 88 | private readonly Dictionary m_Platforms = new Dictionary(); 89 | } 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/DataDrivenPlatformInfo.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Hierarchy 2 | { 3 | public struct DataDrivenPlatformInfo 4 | { 5 | public string PlatformIdentifier; 6 | public string ParentPlatformIdentifier; 7 | } 8 | } -------------------------------------------------------------------------------- /UE4Config/Hierarchy/DataDrivenPlatformProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UE4Config.Evaluation; 4 | using UE4Config.Parsing; 5 | 6 | namespace UE4Config.Hierarchy 7 | { 8 | /// 9 | /// A class that supports the DataDrivenPlatformInfo setup introduced in Unreal Engine 4.23 10 | /// 11 | public class DataDrivenPlatformProvider 12 | { 13 | public IConfigFileProviderAutoPlatformModel FileProvider { get; private set; } 14 | 15 | public IReadOnlyDictionary DataDrivenPlatformInfos => m_DataDrivenPlatformInfos; 16 | 17 | protected Dictionary m_DataDrivenPlatformInfos { get; } = 18 | new Dictionary(); 19 | 20 | public void Setup(IConfigFileProviderAutoPlatformModel configFileProvider) 21 | { 22 | FileProvider = configFileProvider; 23 | } 24 | 25 | public void CollectDataDrivenPlatforms() 26 | { 27 | if (FileProvider == null) 28 | { 29 | throw new NullReferenceException("FileProvider was null"); 30 | } 31 | 32 | //Fetch all DataDrivenPlatformInfo.ini's, read them, construct platforms etc. 33 | var platforms = FileProvider.FindAllPlatforms(); 34 | m_DataDrivenPlatformInfos.Clear(); 35 | var dataDrivenPlatformInfos = m_DataDrivenPlatformInfos; 36 | ConfigIni dataDrivenPlatformConfig; 37 | foreach (var platformIdentifier in platforms) 38 | { 39 | if (FileProvider.LoadOrCreateDataDrivenPlatformConfig(platformIdentifier, out dataDrivenPlatformConfig)) 40 | { 41 | ParseDataDrivenPlatformInfos(dataDrivenPlatformConfig, info => dataDrivenPlatformInfos.Add(info.PlatformIdentifier, info)); 42 | } 43 | } 44 | } 45 | 46 | protected void ParseDataDrivenPlatformInfos(ConfigIni dataDrivenPlatformConfig, Action onDataDrivenPlatformInfo) 47 | { 48 | if (dataDrivenPlatformConfig == null) 49 | { 50 | return; 51 | } 52 | foreach (var iniSection in dataDrivenPlatformConfig.Sections) 53 | { 54 | const string platformInfoHeaderPrefix = "PlatformInfo "; 55 | string sectionName = iniSection.Name; 56 | if(sectionName.Length <= platformInfoHeaderPrefix.Length || !sectionName.StartsWith(platformInfoHeaderPrefix)) 57 | continue; 58 | 59 | var platformInfo = new DataDrivenPlatformInfo(); 60 | platformInfo.PlatformIdentifier = iniSection.Name.Substring(platformInfoHeaderPrefix.Length); 61 | List results = new List(); 62 | 63 | results.Clear(); 64 | PropertyEvaluator.Default.EvaluatePropertyValues(dataDrivenPlatformConfig, sectionName, "TargetPlatformName", results); 65 | if (results.Count > 0) 66 | { 67 | platformInfo.PlatformIdentifier = results[0]; 68 | } 69 | 70 | results.Clear(); 71 | PropertyEvaluator.Default.EvaluatePropertyValues(dataDrivenPlatformConfig, sectionName, "IniPlatformName", results); 72 | if (results.Count > 0) 73 | { 74 | platformInfo.ParentPlatformIdentifier = results[0]; 75 | } 76 | 77 | onDataDrivenPlatformInfo(platformInfo); 78 | } 79 | } 80 | 81 | public void RegisterDataDrivenPlatforms(IConfigReferenceTree referenceTree) 82 | { 83 | var dataDrivenPlatformInfos = m_DataDrivenPlatformInfos; 84 | var visitedPlatformIdentifiers = new List(); 85 | var hierarchyTemp = new List(); 86 | foreach (var dataDrivenPlatformInfo in dataDrivenPlatformInfos.Values) 87 | { 88 | //To add in correct order for valid tree structure, add roots&parents first 89 | if (visitedPlatformIdentifiers.Contains(dataDrivenPlatformInfo.PlatformIdentifier)) 90 | { 91 | continue; 92 | } 93 | hierarchyTemp.Clear(); 94 | FindDataDrivenPlatformInfoHierarchy(dataDrivenPlatformInfo, dataDrivenPlatformInfos, ref hierarchyTemp); 95 | foreach (var dataDrivenPlatformInfoInHierarchy in hierarchyTemp) 96 | { 97 | if (visitedPlatformIdentifiers.Contains(dataDrivenPlatformInfoInHierarchy.PlatformIdentifier)) 98 | { 99 | continue; 100 | } 101 | 102 | visitedPlatformIdentifiers.Add(dataDrivenPlatformInfoInHierarchy.PlatformIdentifier); 103 | 104 | IConfigPlatform parentPlatform = null; 105 | if (!String.IsNullOrEmpty(dataDrivenPlatformInfoInHierarchy.ParentPlatformIdentifier)) 106 | { 107 | parentPlatform = referenceTree.GetPlatform(dataDrivenPlatformInfoInHierarchy.ParentPlatformIdentifier); 108 | } 109 | 110 | referenceTree.RegisterPlatform(dataDrivenPlatformInfoInHierarchy.PlatformIdentifier, 111 | parentPlatform); 112 | } 113 | } 114 | } 115 | 116 | protected void FindDataDrivenPlatformInfoHierarchy(DataDrivenPlatformInfo platformInfo, Dictionary platformInfos, ref List hierarchy) 117 | { 118 | DataDrivenPlatformInfo pivotPlatformInfo = platformInfo; 119 | do 120 | { 121 | if (hierarchy.Contains(pivotPlatformInfo)) 122 | { 123 | break; // Loop detected 124 | } 125 | 126 | hierarchy.Add(pivotPlatformInfo); 127 | } while (FindParentDataDrivenPlatformInfo(pivotPlatformInfo, platformInfos, out pivotPlatformInfo)); 128 | 129 | hierarchy.Reverse(); 130 | } 131 | 132 | protected bool FindParentDataDrivenPlatformInfo(DataDrivenPlatformInfo platformInfo, Dictionary platformInfos, out DataDrivenPlatformInfo parentPlatformInfo) 133 | { 134 | if (!String.IsNullOrEmpty(platformInfo.ParentPlatformIdentifier)) 135 | { 136 | if (platformInfos.TryGetValue(platformInfo.ParentPlatformIdentifier, out var parentPlatformInfoCandidate)) 137 | { 138 | parentPlatformInfo = parentPlatformInfoCandidate; 139 | return true; 140 | } 141 | else 142 | { 143 | //Create a stand-in for the unresolved platform 144 | parentPlatformInfo = new DataDrivenPlatformInfo() 145 | { 146 | PlatformIdentifier = platformInfo.ParentPlatformIdentifier 147 | }; 148 | return true; 149 | } 150 | } 151 | 152 | parentPlatformInfo = new DataDrivenPlatformInfo(); 153 | return false; 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /UE4Config/Hierarchy/FileConfigHierarchy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.IO; 5 | using UE4Config.Parsing; 6 | 7 | namespace UE4Config.Hierarchy 8 | { 9 | /// 10 | /// Helps load config files in hierarchical chains, emulating the Unreal Engine toolset. 11 | /// Allows fetching specific configs as well as evaluating a propertys values at any level of the hierarchy. 12 | /// 13 | [Obsolete] 14 | public class FileConfigHierarchy : ConfigHierarchy 15 | { 16 | /// 17 | /// The path to the engine directory (called and including 'Engine') 18 | /// 19 | public string EnginePath 20 | { 21 | get; 22 | protected set; 23 | } 24 | 25 | /// 26 | /// The path to the project root directory (which contains the *.uproject file) 27 | /// 28 | public string ProjectPath 29 | { 30 | get; 31 | protected set; 32 | } 33 | 34 | public FileConfigHierarchy(string projectPath, string enginePath) 35 | { 36 | EnginePath = enginePath; 37 | ProjectPath = projectPath; 38 | } 39 | 40 | public override ConfigIni GetConfig(string platform, string category, ConfigHierarchyLevel level) 41 | { 42 | ConfigIni config = null; 43 | if (!IsConfigCached(platform, category, level)) 44 | { 45 | config = LoadConfig(platform, category, level); 46 | CacheConfig(platform, category, level, config); 47 | } 48 | else 49 | { 50 | config = GetCachedConfig(platform, category, level); 51 | } 52 | 53 | return config; 54 | } 55 | 56 | public override void PublishConfig(string platform, string category, ConfigHierarchyLevel level, ConfigIni config) 57 | { 58 | CacheConfig(platform, category, level, config); 59 | SaveConfig(platform, category, level, config); 60 | } 61 | 62 | public override ConfigIni CreateConfig(string platform, string category, ConfigHierarchyLevel level) 63 | { 64 | var filePath = GetConfigFilePath(platform, category, level); 65 | if (filePath == null) 66 | return null; 67 | 68 | ConfigIni config = new ConfigIni(filePath); 69 | CacheConfig(platform, category, level, config); 70 | return config; 71 | } 72 | 73 | public override bool CheckEngineHasPlatformExtension(string platform) 74 | { 75 | string expectedPath = GenerateEnginePlatformExtensionConfigDirPath(platform); 76 | return Directory.Exists(expectedPath); 77 | } 78 | 79 | public override bool CheckProjectHasPlatformExtension(string platform) 80 | { 81 | string expectedPath = GenerateProjectPlatformExtensionConfigDirPath(platform); 82 | return Directory.Exists(expectedPath); 83 | } 84 | 85 | /// 86 | /// Constructs the file path that would lead to the requested config file 87 | /// 88 | public virtual string GetConfigFilePath(string platform, string category, ConfigHierarchyLevel level) 89 | { 90 | switch (level) 91 | { 92 | case ConfigHierarchyLevel.Base: 93 | return Path.Combine(Environment.CurrentDirectory, EnginePath, ConfigDirName, $"{BaseConfigFilePrefix}.ini"); 94 | case ConfigHierarchyLevel.BaseCategory: 95 | return Path.Combine(Environment.CurrentDirectory, EnginePath, ConfigDirName, $"{BaseConfigFilePrefix}{category}.ini"); 96 | case ConfigHierarchyLevel.BasePlatformCategory: 97 | if (platform == DefaultPlatform) 98 | { 99 | return null; 100 | } 101 | else 102 | { 103 | if (CheckEngineHasPlatformExtension(platform)) 104 | { 105 | //Return new config path 106 | return Path.Combine(GenerateEnginePlatformExtensionConfigDirPath(platform), $"{platform}{category}.ini"); 107 | } 108 | else 109 | { 110 | //Return legacy config path 111 | return Path.Combine(Environment.CurrentDirectory, EnginePath, ConfigDirName, platform, $"{platform}{category}.ini"); 112 | } 113 | } 114 | case ConfigHierarchyLevel.ProjectCategory: 115 | return Path.Combine(Environment.CurrentDirectory, ProjectPath, ConfigDirName, $"{DefaultConfigFilePrefix}{category}.ini"); 116 | case ConfigHierarchyLevel.ProjectPlatformCategory: 117 | if (platform == DefaultPlatform) 118 | { 119 | return null; 120 | } 121 | else 122 | { 123 | if (CheckProjectHasPlatformExtension(platform)) 124 | { 125 | //Return new config path 126 | return Path.Combine(GenerateProjectPlatformExtensionConfigDirPath(platform), $"{platform}{category}.ini"); 127 | } 128 | else 129 | { 130 | //Return legacy config path 131 | return Path.Combine(Environment.CurrentDirectory, ProjectPath, ConfigDirName, platform, $"{platform}{category}.ini"); 132 | } 133 | } 134 | default: 135 | throw new InvalidEnumArgumentException(nameof(level), (int)level, typeof(ConfigHierarchyLevel)); 136 | } 137 | } 138 | 139 | /// 140 | /// Load config from filesystem 141 | /// 142 | protected virtual ConfigIni LoadConfig(string platform, string category, ConfigHierarchyLevel level) 143 | { 144 | var filePath = GetConfigFilePath(platform, category, level); 145 | if (filePath == null) 146 | return null; 147 | 148 | StreamReader reader; 149 | try 150 | { 151 | reader = File.OpenText(filePath); 152 | } 153 | catch (DirectoryNotFoundException) 154 | { 155 | return null; 156 | } 157 | catch (FileNotFoundException) 158 | { 159 | return null; 160 | } 161 | var config = new ConfigIni(Path.GetFileName(filePath)); 162 | config.Read(reader); 163 | reader.Close(); 164 | return config; 165 | } 166 | 167 | protected virtual void SaveConfig(string platform, string category, ConfigHierarchyLevel level, ConfigIni config) 168 | { 169 | var filePath = GetConfigFilePath(platform, category, level); 170 | FileStream fileStream; 171 | fileStream = File.OpenWrite(filePath); 172 | var writer = new ConfigIniWriter(new StreamWriter(fileStream)); 173 | config.Write(writer); 174 | writer.ContentWriter.Close(); 175 | } 176 | 177 | protected virtual void CacheConfig(string platform, string category, ConfigHierarchyLevel level, ConfigIni config) 178 | { 179 | var key = new ConfigKey(platform, category, level); 180 | m_ConfigCache[key] = config; 181 | } 182 | 183 | protected virtual ConfigIni GetCachedConfig(string platform, string category, ConfigHierarchyLevel level) 184 | { 185 | ConfigIni result; 186 | if (m_ConfigCache.TryGetValue(new ConfigKey(platform, category, level), out result)) 187 | { 188 | return result; 189 | } 190 | return null; 191 | } 192 | 193 | protected virtual bool IsConfigCached(string platform, string category, ConfigHierarchyLevel level) 194 | { 195 | return m_ConfigCache.ContainsKey(new ConfigKey(platform, category, level)); 196 | } 197 | 198 | protected string GenerateEnginePlatformExtensionConfigDirPath(string platform) 199 | { 200 | return Path.Combine(Environment.CurrentDirectory, EnginePath, PlatformExtensionsBaseDirName, platform, ConfigDirName); 201 | } 202 | 203 | protected string GenerateProjectPlatformExtensionConfigDirPath(string platform) 204 | { 205 | return Path.Combine(Environment.CurrentDirectory, ProjectPath, PlatformExtensionsBaseDirName, platform, ConfigDirName); 206 | } 207 | 208 | private struct ConfigKey 209 | { 210 | public ConfigKey(string platform, string category, ConfigHierarchyLevel level) 211 | { 212 | Platform = platform; 213 | Category = category; 214 | Level = level; 215 | } 216 | 217 | public string Platform; 218 | public string Category; 219 | public ConfigHierarchyLevel Level; 220 | } 221 | 222 | private Dictionary m_ConfigCache = new Dictionary(); 223 | 224 | private const string ConfigDirName = "Config"; 225 | private const string BaseConfigFilePrefix = "Base"; 226 | private const string DefaultConfigFilePrefix = "Default"; 227 | private const string PlatformExtensionsBaseDirName = "Platforms"; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/IConfigFileIOAdapter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | public interface IConfigFileIOAdapter 7 | { 8 | /// 9 | /// Returns the paths of all subdirectories at path, excluding path itself. 10 | /// 11 | List GetDirectories(string pivotPath); 12 | 13 | StreamReader OpenRead(string filePath); 14 | 15 | StreamWriter OpenWrite(string filePath); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/IConfigFileProvider.cs: -------------------------------------------------------------------------------- 1 | using UE4Config.Parsing; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | public interface IConfigFileProvider 6 | { 7 | /// 8 | /// The ConfigFileIOAdapter used to retrieve information and contents of the file storage 9 | /// 10 | IConfigFileIOAdapter FileIOAdapter { get; } 11 | 12 | /// 13 | /// Base path to the engine directory (containing e.g. the /Engine and /Source subdirectory). 14 | /// Can be null to ignore. 15 | /// 16 | string EnginePath { get; } 17 | 18 | /// 19 | /// Base path to the project directory (containing the *.uproject file). 20 | /// Can be null to ignore. 21 | /// 22 | string ProjectPath { get; } 23 | 24 | bool IsSetup { get; } 25 | 26 | /// 27 | /// Sets up the provider with base paths. 28 | /// 29 | /// 30 | /// The ConfigFileIOAdapter used to retrieve information and contents of the file storage 31 | /// 32 | /// 33 | /// Base path to the engine directory (containing e.g. the /Engine and /Source subdirectory). 34 | /// Can be null to ignore. 35 | /// 36 | /// 37 | /// Base path to the project directory (containing the *.uproject file). 38 | /// Can be null to ignore. 39 | /// 40 | void Setup(IConfigFileIOAdapter fileIOAdapter, string enginePath, string projectPath); 41 | 42 | /// 43 | /// Returns a string filepath to a config file pointed at by the reference. 44 | /// Returns null if the reference cannot be resolved. 45 | /// 46 | string ResolveConfigFilePath(ConfigFileReference reference); 47 | 48 | /// 49 | /// Loads a config from the filesystem if it exists or creates a new one. 50 | /// Returns true if the config was loaded. 51 | /// 52 | bool LoadOrCreateConfig(ConfigFileReference configFileReference, out ConfigIni configIni); 53 | 54 | /// 55 | /// Loads a config from the filesystem if it exists or creates a new one. 56 | /// Returns true if the config was loaded. 57 | /// 58 | bool LoadOrCreateDataDrivenPlatformConfig(string platformIdentifier, out ConfigIni configIni); 59 | 60 | /// 61 | /// Saves a config to the filesystem, overwriting a possibly existing one. 62 | /// Will not save if there were no meaningful changes. 63 | /// 64 | void SaveConfig(ConfigFileReference configFileReference, ConfigIni configIni); 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/IConfigFileProviderAutoPlatformModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | public interface IConfigFileProviderAutoPlatformModel : IConfigFileProvider 6 | { 7 | /// 8 | /// Finds and returns all platform identifiers that seem to have a config directory in either engine or project 9 | /// 10 | List FindAllPlatforms(); 11 | 12 | /// 13 | /// Automatically scans the and 14 | /// for all Platforms that require legacy config setup. 15 | /// Will likely override manual settings made before, but will leave undiscovered platforms intact. 16 | /// 17 | void AutoDetectPlatformsUsingLegacyConfig(); 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/IConfigPlatform.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | /// 6 | /// Platforms are optional additional layers in the hierarchy that only apply to 7 | /// specific target platforms. 8 | /// Platforms support additional subhierarchies by inheritance e.g. "PS4" and "PS5" inheriting config from "Sony" 9 | /// 10 | public interface IConfigPlatform 11 | { 12 | string Identifier { get; } 13 | IConfigPlatform ParentPlatform { get; } 14 | } 15 | 16 | public static class IConfigPlatformExtensions 17 | { 18 | public static void ResolvePlatformInheritance(this IConfigPlatform platform, ref List outPlatforms) 19 | { 20 | var platforms = new List(); 21 | var pivotPlatform = platform; 22 | while (pivotPlatform != null) 23 | { 24 | platforms.Add(pivotPlatform); 25 | pivotPlatform = pivotPlatform.ParentPlatform; 26 | }; 27 | platforms.Reverse(); 28 | outPlatforms.AddRange(platforms); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/IConfigReferenceTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | /// 7 | /// A configuration tree provides a common interface to generate chains of configuration files ("branches") 8 | /// for any type/platform combination. 9 | /// 10 | /// 11 | /// This replaces the ConfigHierarchy used in older versions of this codebase. 12 | /// This does not support layer-hierarchies below UE427+ 13 | /// This does not yet support user-layers 14 | /// 15 | public interface IConfigReferenceTree 16 | { 17 | /** 18 | * The provided during 19 | */ 20 | IConfigFileProvider FileProvider { get; } 21 | /** 22 | * Provides dependencies and initialized the reference tree to return correct root and branches. 23 | */ 24 | void Setup(IConfigFileProvider configFileProvider); 25 | /** 26 | * Calls for the root of the reference tree. 27 | */ 28 | void VisitConfigRoot(Action onConfig); 29 | /** 30 | * Calls for every config in designated branch of the reference tree, 31 | * including and starting with the root. 32 | */ 33 | void VisitConfigBranch(string configType, string platformIdentifier, Action onConfig); 34 | IConfigPlatform GetPlatform(string platformIdentifier); 35 | IConfigPlatform RegisterPlatform(string platformIdentifier, IConfigPlatform parentPlatform = null); 36 | } 37 | 38 | /** 39 | * Static utility methods to work with s 40 | */ 41 | public static class IConfigReferenceTreeExtensions 42 | { 43 | /** 44 | * Returns a file reference to the supposed config file serving as the root of the tree 45 | */ 46 | public static ConfigFileReference? GetConfigRoot(this IConfigReferenceTree configReferenceTree) 47 | { 48 | ConfigFileReference? result = null; 49 | configReferenceTree.VisitConfigRoot(reference => result = reference); 50 | return result; 51 | } 52 | 53 | /** 54 | * Returns a list of file references to the supposed config files of the designated branch of the tree, 55 | * including and starting with the root. 56 | */ 57 | public static List GetConfigBranch(this IConfigReferenceTree configReferenceTree, string configType, string platformIdentifier) 58 | { 59 | var result = new List(); 60 | configReferenceTree.VisitConfigBranch(configType, platformIdentifier, reference => result.Add(reference)); 61 | return result; 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/VirtualConfigCache.cs: -------------------------------------------------------------------------------- 1 | using UE4Config.Parsing; 2 | 3 | namespace UE4Config.Hierarchy 4 | { 5 | /// 6 | /// Wraps a single possibly cached 7 | /// 8 | public class VirtualConfigCache 9 | { 10 | public enum ConfigFileState 11 | { 12 | Unknown, 13 | /// 14 | /// Config has been loaded from file 15 | /// 16 | Loaded, 17 | /// 18 | /// Config has been created in memory because file could not be loaded 19 | /// 20 | Created 21 | } 22 | 23 | public ConfigFileReference FileReference { get; private set; } 24 | public ConfigFileState FileState { get; private set; } 25 | public ConfigIni ConfigIni { get; private set; } 26 | 27 | public VirtualConfigCache(ConfigFileReference configFileReference) 28 | { 29 | FileReference = configFileReference; 30 | FileState = ConfigFileState.Unknown; 31 | } 32 | 33 | public void SetConfigIni(ConfigIni configIni, bool wasLoaded) 34 | { 35 | ConfigIni = configIni; 36 | FileState = wasLoaded ? ConfigFileState.Loaded : ConfigFileState.Created; 37 | } 38 | 39 | public void InvalidateCache() 40 | { 41 | FileState = ConfigFileState.Unknown; 42 | ConfigIni = null; 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/VirtualConfigTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UE4Config.Parsing; 4 | 5 | namespace UE4Config.Hierarchy 6 | { 7 | /// 8 | /// The virtual configuration tree provides a bundled solution to manage in-memory version of the reference config tree. 9 | /// If effectively caches configs of the underlying tree and allows you to create, load, reload and publish them. 10 | /// 11 | public class VirtualConfigTree 12 | { 13 | public IConfigReferenceTree ReferenceTree { get; private set; } 14 | public IConfigFileProvider FileProvider => ReferenceTree.FileProvider; 15 | public VirtualConfigsCache ConfigsCache { get; private set; } 16 | 17 | public VirtualConfigTree(IConfigReferenceTree referenceTree) 18 | { 19 | ReferenceTree = referenceTree; 20 | ConfigsCache = new VirtualConfigsCache(); 21 | } 22 | 23 | /// 24 | /// Calls passing the root config as referenced by . 25 | /// Either passes the cached instance or creates a new instance, loading contents from disk if possible. 26 | /// 27 | public void VisitConfigRoot(Action onConfig) 28 | { 29 | ReferenceTree.VisitConfigRoot(reference => 30 | { 31 | var config = ConfigsCache.GetOrLoadConfig(reference, FileProvider); 32 | onConfig?.Invoke(config); 33 | }); 34 | } 35 | 36 | /// 37 | /// Calls passing every config on a branch referenced by . 38 | /// Either passes the cached instances or creates new instances, loading contents from disk if possible. 39 | /// 40 | public void VisitConfigBranch(string configType, string platformIdentifier, Action onConfig) 41 | { 42 | ReferenceTree.VisitConfigBranch(configType, platformIdentifier, reference => 43 | { 44 | var config = ConfigsCache.GetOrLoadConfig(reference, FileProvider); 45 | onConfig?.Invoke(config); 46 | }); 47 | } 48 | 49 | /// 50 | /// Utility method wrapping . 51 | /// Returns the root config as referenced by . 52 | /// Either returns the cached instance or creates a new instance, loading contents from disk if possible. 53 | /// 54 | public ConfigIni FetchConfigRoot() 55 | { 56 | ConfigIni result = null; 57 | VisitConfigRoot(config => 58 | { 59 | result = config; 60 | }); 61 | return result; 62 | } 63 | 64 | /// 65 | /// Utility method wrapping . 66 | /// Returns every config on a branch referenced by . 67 | /// Either returns the cached instances or creates new instances, loading contents from disk if possible. 68 | /// 69 | public List FetchConfigBranch(string configType, string platformIdentifier) 70 | { 71 | List result = new List(); 72 | VisitConfigBranch(configType, platformIdentifier, config => 73 | { 74 | result.Add(config); 75 | }); 76 | return result; 77 | } 78 | 79 | /// 80 | /// Caches the given config instance, possibly overwriting a currently cached instance, before also 81 | /// saving it to disk. Use the of the given as 82 | /// cache identifier. 83 | /// 84 | public void PublishConfig(ConfigIni configIni) 85 | { 86 | if (configIni == null) 87 | throw new ArgumentNullException(nameof(configIni)); 88 | var configReference = configIni.Reference; 89 | var cache = ConfigsCache.Peek(configReference); 90 | FileProvider.SaveConfig(configReference, configIni); 91 | cache.SetConfigIni(configIni, true); 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/VirtualConfigTreeUtility.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Hierarchy 2 | { 3 | public static class VirtualConfigTreeUtility 4 | { 5 | /// 6 | /// Utility method to quickly create a default virtual config tree instance for common use 7 | /// 8 | public static VirtualConfigTree CreateVirtualConfigTree(string enginePath, string projectPath) 9 | where TConfigFileProvider : IConfigFileProvider, new() 10 | where TConfigFileIOAdapter : IConfigFileIOAdapter, new() 11 | where TConfigReferenceTree : IConfigReferenceTree, new() 12 | { 13 | //This will provide paths and a virtual hierarchy for a project+engine base path combination 14 | var configProvider = new TConfigFileProvider(); 15 | configProvider.Setup(new TConfigFileIOAdapter(), enginePath, projectPath); 16 | DataDrivenPlatformProvider dataDrivenPlatformProvider = new DataDrivenPlatformProvider(); 17 | if(configProvider is IConfigFileProviderAutoPlatformModel configProviderAuto) 18 | { 19 | //Auto-detect if a project still uses the legacy Config/{Platform}/*.ini setup. 20 | configProviderAuto.AutoDetectPlatformsUsingLegacyConfig(); 21 | //Create a DataDrivenPlatform provider that can pre-fill our platform hierarchy like UE4.27 does (based on DataDrivenPlatform configs) 22 | dataDrivenPlatformProvider.Setup(configProviderAuto); 23 | dataDrivenPlatformProvider.CollectDataDrivenPlatforms(); 24 | } 25 | //Create the base tree model, based on the config hierarchy used by UE since 4.27+ 26 | var configRefTree = new TConfigReferenceTree(); 27 | configRefTree.Setup(configProvider); 28 | //Apply any detected DataDrivenPlatforms 29 | dataDrivenPlatformProvider.RegisterDataDrivenPlatforms(configRefTree); 30 | //Create a virtual config tree to allow us working with an in-memory virtual hierarchy 31 | var configTree = new VirtualConfigTree(configRefTree); 32 | return configTree; 33 | } 34 | 35 | public static VirtualConfigTree CreateVirtualConfigTree(string enginePath, string projectPath) 36 | { 37 | return CreateVirtualConfigTree(enginePath, projectPath); 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /UE4Config/Hierarchy/VirtualConfigsCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UE4Config.Parsing; 3 | 4 | namespace UE4Config.Hierarchy 5 | { 6 | /// 7 | /// Stores multiple based on their 8 | /// 9 | public class VirtualConfigsCache 10 | { 11 | public VirtualConfigCache Peek(ConfigFileReference configFileReference) 12 | { 13 | if (!m_Cache.ContainsKey(configFileReference)) 14 | { 15 | m_Cache[configFileReference] = new VirtualConfigCache(configFileReference); 16 | } 17 | 18 | return m_Cache[configFileReference]; 19 | } 20 | 21 | public ConfigIni GetOrLoadConfig(ConfigFileReference configFileReference, IConfigFileProvider fileProvider) 22 | { 23 | var cache = Peek(configFileReference); 24 | if (cache.ConfigIni == null) 25 | { 26 | bool wasLoaded = fileProvider.LoadOrCreateConfig(configFileReference, out var configIni); 27 | cache.SetConfigIni(configIni, wasLoaded); 28 | } 29 | return cache.ConfigIni; 30 | } 31 | 32 | public void InvalidateCache() 33 | { 34 | foreach (var cacheEntry in m_Cache) 35 | { 36 | cacheEntry.Value.InvalidateCache(); 37 | } 38 | } 39 | 40 | private Dictionary m_Cache = new Dictionary(); 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /UE4Config/Parsing/CommentToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UE4Config.Parsing 4 | { 5 | public class CommentToken : MultilineToken 6 | { 7 | public CommentToken() : base() { } 8 | 9 | public CommentToken(IEnumerable lines, LineEnding lineEnding) : base(lines, lineEnding) {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UE4Config/Parsing/ConfigIniSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UE4Config.Parsing 5 | { 6 | /** 7 | * Represents a single named group of s 8 | */ 9 | public class ConfigIniSection 10 | { 11 | public string Name {get;set;} 12 | public List Tokens {get;} = new List(); 13 | public LineEnding LineEnding {get;set;} 14 | 15 | /// 16 | /// Trimmed whitespace characters prefixing this sections header line 17 | /// 18 | public string LineWastePrefix {get;set;} 19 | /// 20 | /// Trimmed whitespace characters suffixing this sections header line 21 | /// 22 | public string LineWasteSuffix {get;set;} 23 | 24 | public ConfigIniSection() { } 25 | 26 | public ConfigIniSection(string name) 27 | { 28 | Name = name; 29 | } 30 | 31 | public ConfigIniSection(IEnumerable tokens) 32 | { 33 | if (tokens != null) 34 | { 35 | Tokens.AddRange(tokens); 36 | } 37 | } 38 | 39 | public ConfigIniSection(string name, IEnumerable tokens) 40 | { 41 | Name = name; 42 | if (tokens != null) 43 | { 44 | Tokens.AddRange(tokens); 45 | } 46 | } 47 | 48 | public static ConfigIniSection Clone(ConfigIniSection template) 49 | { 50 | var cloned = new ConfigIniSection(template.Name) 51 | { 52 | LineEnding = template.LineEnding, 53 | LineWastePrefix = template.LineWastePrefix, 54 | LineWasteSuffix = template.LineWasteSuffix 55 | }; 56 | foreach (var templateToken in template.Tokens) 57 | { 58 | var clonedToken = templateToken.CreateClone(); 59 | cloned.Tokens.Add(clonedToken); 60 | } 61 | return cloned; 62 | } 63 | 64 | /// 65 | /// Adds all instructions that could be found for the given key to the list, in order of declaration 66 | /// 67 | public void FindPropertyInstructions(string propertyKey, IList instructions) 68 | { 69 | foreach (var iniToken in Tokens) 70 | { 71 | if (iniToken is InstructionToken instruction) 72 | { 73 | if (instruction.Key == propertyKey) 74 | { 75 | instructions.Add(instruction); 76 | } 77 | } 78 | } 79 | } 80 | 81 | public IniToken GetLastToken() 82 | { 83 | if (Tokens.Count > 0) 84 | { 85 | return Tokens[Tokens.Count - 1]; 86 | } 87 | 88 | return null; 89 | } 90 | 91 | /// 92 | /// Groups together instruction tokens, keeping their order of declaration intact 93 | /// 94 | public void GroupPropertyInstructions() 95 | { 96 | for (int i = Tokens.Count - 1; i > 0; i--) 97 | { 98 | var token = Tokens[i]; 99 | if (token is InstructionToken instruction) 100 | { 101 | FindPropertyInstructionGroup(instruction.Key, out _, out int lastIndex); 102 | if (lastIndex < i) 103 | { 104 | Tokens.RemoveAt(i); 105 | Tokens.Insert(lastIndex+1, token); 106 | } 107 | } 108 | } 109 | } 110 | 111 | protected void FindPropertyInstructionGroup(string key, out int indexOfFirstInstruction, out int indexOfLastInstruction) 112 | { 113 | indexOfFirstInstruction = -1; 114 | indexOfLastInstruction = -1; 115 | for (int i = 0; i < Tokens.Count; i++) 116 | { 117 | var token = Tokens[i]; 118 | bool isPartOfGroup = false; 119 | if (token is InstructionToken instruction) 120 | { 121 | isPartOfGroup = instruction.Key == key; 122 | } 123 | 124 | if (isPartOfGroup && indexOfFirstInstruction < 0) 125 | { 126 | indexOfFirstInstruction = i; 127 | } 128 | else if (!isPartOfGroup && indexOfFirstInstruction >= 0) 129 | { 130 | indexOfLastInstruction = i - 1; 131 | break; 132 | } 133 | } 134 | } 135 | 136 | /// 137 | /// Merges together supported consecutive tokens of the same type. 138 | /// This includes and 139 | /// 140 | public void MergeConsecutiveTokens() 141 | { 142 | for (int i = Tokens.Count - 1; i > 0; i--) 143 | { 144 | var token = Tokens[i]; 145 | var precedingToken = Tokens[i - 1]; 146 | 147 | if (token is WhitespaceToken whitespace) 148 | { 149 | if (precedingToken is WhitespaceToken precedingWhitespace) 150 | { 151 | precedingWhitespace.Lines.AddRange(whitespace.Lines); 152 | Tokens.RemoveAt(i); 153 | } 154 | } else if (token is CommentToken comment) 155 | { 156 | if (precedingToken is CommentToken precedingComment) 157 | { 158 | precedingComment.Lines.AddRange(comment.Lines); 159 | Tokens.RemoveAt(i); 160 | } 161 | } 162 | } 163 | } 164 | 165 | /// 166 | /// Condenses all whitespace to a maximum one newline. 167 | /// . 168 | /// Note that consecutive whitespace tokens might still results in multiple newlines. 169 | /// Use before to avoid that. 170 | /// 171 | public void CondenseWhitespace() 172 | { 173 | for (int i = Tokens.Count - 1; i > 0; i--) 174 | { 175 | var token = Tokens[i]; 176 | 177 | if (token is WhitespaceToken whitespace) 178 | { 179 | whitespace.Condense(); 180 | } 181 | } 182 | } 183 | 184 | /// 185 | /// Applies the given lineEnding to the section header and all tokens 186 | /// 187 | public void NormalizeLineEndings(LineEnding lineEnding) 188 | { 189 | LineEnding = lineEnding; 190 | foreach (var token in Tokens) 191 | { 192 | 193 | if (token is LineToken lineToken) 194 | { 195 | lineToken.LineEnding = lineEnding; 196 | } 197 | else if (token is MultilineToken multilineToken) 198 | { 199 | for (int t = 0; t < multilineToken.Lines.Count; t++) 200 | { 201 | var line = multilineToken.Lines[t]; 202 | line.LineEnding = lineEnding; 203 | multilineToken.Lines[t] = line; 204 | } 205 | } 206 | } 207 | } 208 | 209 | /// 210 | /// Writes this sections to a string 211 | /// 212 | public virtual void Write(ConfigIniWriter writer) 213 | { 214 | WriteHeader(writer); 215 | WriteTokens(writer); 216 | } 217 | 218 | /// 219 | /// Writes this sections header to a text blob 220 | /// 221 | public void WriteHeader(ConfigIniWriter writer) 222 | { 223 | if (Name != null) 224 | { 225 | if (!String.IsNullOrEmpty(LineWastePrefix)) 226 | { 227 | writer.Write(LineWastePrefix); 228 | } 229 | 230 | writer.Write($"[{Name}]"); 231 | 232 | if (!String.IsNullOrEmpty(LineWasteSuffix)) 233 | { 234 | writer.Write(LineWasteSuffix); 235 | } 236 | LineEnding.WriteTo(writer); //Finish the line 237 | } 238 | //If there is no Name but there is line-waste, we consider this a parsing error. 239 | } 240 | 241 | /// 242 | /// Writes this sections to a text blob 243 | /// 244 | /// 245 | public virtual void WriteTokens(ConfigIniWriter writer) 246 | { 247 | foreach (var token in Tokens) 248 | { 249 | if (token != null) 250 | { 251 | token.Write(writer); 252 | } 253 | } 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /UE4Config/Parsing/ConfigIniWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace UE4Config.Parsing 4 | { 5 | /// 6 | /// A wrapper for writers of ConfigIni files. 7 | /// 8 | public class ConfigIniWriter 9 | { 10 | public TextWriter ContentWriter = null; 11 | public LineEnding LineEnding = LineEnding.Unknown; 12 | /// 13 | /// The Unreal Engine 4 config file serializer has a quirk that always makes the file end with 2 line endings 14 | /// (one for the last token, one empty line). 15 | /// Enable this to keep consistent with UE4 and avoid additional changes e.g. in source control. 16 | /// 17 | public bool AppendQuirkFileEnding = false; 18 | 19 | public ConfigIniWriter(TextWriter textContentWriter) 20 | { 21 | ContentWriter = textContentWriter; 22 | } 23 | 24 | public void Write(string content) 25 | { 26 | ContentWriter.Write(content); 27 | } 28 | 29 | public override string ToString() 30 | { 31 | var writtenString = ContentWriter.ToString(); 32 | if (AppendQuirkFileEnding) 33 | { 34 | var lineEndingStr = LineEnding.AsString(); 35 | if (!writtenString.EndsWith(lineEndingStr+lineEndingStr)) 36 | { 37 | if (writtenString.EndsWith(lineEndingStr)) 38 | { 39 | writtenString += lineEndingStr; 40 | } 41 | else 42 | { 43 | writtenString += lineEndingStr; 44 | writtenString += lineEndingStr; 45 | } 46 | } 47 | } 48 | return writtenString; 49 | } 50 | 51 | public void WriteLineEnding(LineEnding lineEnding) 52 | { 53 | lineEnding.WriteTo(ContentWriter); 54 | if (LineEnding != lineEnding && LineEnding == LineEnding.Unknown && lineEnding != LineEnding.None) 55 | { 56 | LineEnding = lineEnding; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /UE4Config/Parsing/IniToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UE4Config.Parsing 4 | { 5 | /** 6 | * Represents a single token of UEs configuration INIs. 7 | * Each token usually represents a comment, whitespace or an instruction to modify a named value. 8 | */ 9 | public abstract class IniToken 10 | { 11 | /// 12 | /// Writes this ini token to a text blob 13 | /// 14 | public abstract void Write(ConfigIniWriter writer); 15 | 16 | /// 17 | /// Creates another instance of this token, that contains the same data and can be used else where 18 | /// 19 | public virtual IniToken CreateClone() 20 | { 21 | return (IniToken)Activator.CreateInstance(GetType()); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UE4Config/Parsing/InstructionToken.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Parsing 2 | { 3 | public class InstructionToken : LineToken 4 | { 5 | public InstructionType InstructionType {get;set;} 6 | public string Key {get;set;} 7 | public string Value {get;set;} 8 | 9 | public InstructionToken() { } 10 | 11 | public InstructionToken(InstructionType type, string key, string value = null) 12 | { 13 | InstructionType = type; 14 | Key = key; 15 | Value = value; 16 | } 17 | 18 | public InstructionToken(InstructionType type, string key, LineEnding lineEnding) : base(lineEnding) 19 | { 20 | InstructionType = type; 21 | Key = key; 22 | } 23 | 24 | public InstructionToken(InstructionType type, string key, string value, LineEnding lineEnding) : base(lineEnding) 25 | { 26 | InstructionType = type; 27 | Key = key; 28 | Value = value; 29 | } 30 | 31 | public override IniToken CreateClone() 32 | { 33 | var clone = base.CreateClone() as InstructionToken; 34 | clone.InstructionType = InstructionType; 35 | clone.Key = Key; 36 | clone.Value = Value; 37 | return clone; 38 | } 39 | 40 | public override void Write(ConfigIniWriter writer) 41 | { 42 | writer.Write(InstructionType.AsPrefixString()); 43 | writer.Write(Key); 44 | if (Value != null) 45 | { 46 | writer.Write("="); 47 | writer.Write(Value); 48 | } 49 | LineEnding.WriteTo(writer); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /UE4Config/Parsing/InstructionType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace UE4Config.Parsing 4 | { 5 | public enum InstructionType 6 | { 7 | /// 8 | /// Creates or overrides a key with a value 9 | /// 10 | /// 11 | /// Name=Player 12 | /// 13 | Set, 14 | /// 15 | /// Adds another value to a key or sets its initial value. 16 | /// Subsequent adds of the same value will be ineffective 17 | /// 18 | /// 19 | /// +Maps=SecondLevel.umap 20 | /// 21 | Add, 22 | /// 23 | /// Adds another value to a key or sets its initial value. 24 | /// Will be evaluated as duplicate line, no matter how many times the value has been added before. 25 | /// 26 | /// 27 | /// This is useful for the bindings (as seen in DefaultInput.ini), for instance, 28 | /// where the bottom-most binding takes effect: 29 | /// Bindings=(Name="Q",Command="Foo") 30 | /// .Bindings=(Name="Q",Command="Bar") 31 | /// .Bindings=(Name="Q",Command="Foo") 32 | /// 33 | AddForce, 34 | /// 35 | /// Removes a specific value from a key 36 | /// 37 | /// 38 | /// -Maps=SecondLevel.umap 39 | /// 40 | Remove, 41 | /// 42 | /// Removes all values from a key 43 | /// 44 | /// 45 | /// !Maps 46 | /// 47 | RemoveAll 48 | } 49 | 50 | public static class InstructionTypeExtensions 51 | { 52 | const string PrefixForSetInstruction = ""; 53 | const string PrefixForAddInstruction = "+"; 54 | const string PrefixForAddForceInstruction = "."; 55 | const string PrefixForRemoveInstruction = "-"; 56 | const string PrefixForRemoveAllInstruction = "!"; 57 | 58 | public static string AsPrefixString(this InstructionType type) 59 | { 60 | switch (type) 61 | { 62 | case InstructionType.Set: 63 | return PrefixForSetInstruction; 64 | case InstructionType.Add: 65 | return PrefixForAddInstruction; 66 | case InstructionType.AddForce: 67 | return PrefixForAddForceInstruction; 68 | case InstructionType.Remove: 69 | return PrefixForRemoveInstruction; 70 | case InstructionType.RemoveAll: 71 | return PrefixForRemoveAllInstruction; 72 | default: 73 | throw new InvalidEnumArgumentException(nameof(type), (int)type, typeof(InstructionType)); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /UE4Config/Parsing/LineEnding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | 5 | namespace UE4Config.Parsing 6 | { 7 | public enum LineEnding 8 | { 9 | /// 10 | /// Unknown line-ending - will assume environments default newline 11 | /// 12 | Unknown, 13 | /// 14 | /// No line-ending 15 | /// 16 | None, 17 | /// 18 | /// Newline, Unix and Unix-like systems (Linux, macOS, FreeBSD, AIX, Xenix, etc.) 19 | /// \n 20 | /// 21 | Unix, 22 | /// 23 | /// Newline, Microsoft Windows, DOS (MS-DOS, PC DOS, etc.) 24 | /// \r\n 25 | /// 26 | Windows, 27 | /// 28 | /// Newline, Commodore 8-bit machines (C64, C128), Acorn BBC, ZX Spectrum, TRS-80, Apple II series, Oberon, the classic Mac OS 29 | /// \r 30 | /// 31 | Mac 32 | } 33 | 34 | public static class LineEndingExtensions 35 | { 36 | public static string AsString(this LineEnding lineEnding) 37 | { 38 | switch (lineEnding) 39 | { 40 | case LineEnding.None: 41 | return ""; 42 | case LineEnding.Unix: 43 | return "\n"; 44 | case LineEnding.Windows: 45 | return "\r\n"; 46 | case LineEnding.Mac: 47 | return "\r"; 48 | case LineEnding.Unknown: 49 | return Environment.NewLine; 50 | default: 51 | throw new InvalidEnumArgumentException(nameof(lineEnding), (int)lineEnding, typeof(LineEnding)); 52 | } 53 | } 54 | 55 | public static void WriteTo(this LineEnding lineEnding, TextWriter writer) 56 | { 57 | switch (lineEnding) 58 | { 59 | case LineEnding.None: 60 | break; 61 | case LineEnding.Unknown: 62 | writer.WriteLine(); 63 | break; 64 | default: 65 | writer.Write(lineEnding.AsString()); 66 | break; 67 | } 68 | } 69 | 70 | public static void WriteTo(this LineEnding lineEnding, ConfigIniWriter writer) 71 | { 72 | writer.WriteLineEnding(lineEnding); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /UE4Config/Parsing/LineToken.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Parsing 2 | { 3 | public abstract class LineToken : IniToken 4 | { 5 | public LineEnding LineEnding; 6 | 7 | protected LineToken() { } 8 | 9 | protected LineToken(LineEnding lineEnding) 10 | { 11 | LineEnding = lineEnding; 12 | } 13 | 14 | public override IniToken CreateClone() 15 | { 16 | var clone = base.CreateClone() as LineToken; 17 | clone.LineEnding = LineEnding; 18 | return clone; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /UE4Config/Parsing/MultilineToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UE4Config.Parsing 4 | { 5 | public abstract class MultilineToken : IniToken 6 | { 7 | public List Lines = new List(); 8 | 9 | public void AddLine(string content, LineEnding lineEnding = LineEnding.Unknown) 10 | { 11 | Lines.Add(new TextLine(content, lineEnding)); 12 | } 13 | 14 | public void AddLines(IEnumerable contents, LineEnding lineEnding = LineEnding.Unknown) 15 | { 16 | foreach (var content in contents) 17 | { 18 | AddLine(content, lineEnding); 19 | } 20 | } 21 | 22 | /// 23 | /// Returns all lines converted into an array of strings. 24 | /// 25 | public string[] GetStringLines() 26 | { 27 | string[] strings = new string[Lines.Count]; 28 | for(int i = 0; i < Lines.Count; i++) 29 | { 30 | strings[i] = Lines[i].ToString(); 31 | } 32 | return strings; 33 | } 34 | 35 | 36 | protected MultilineToken() { } 37 | 38 | protected MultilineToken(IEnumerable lines, LineEnding lineEnding) 39 | { 40 | AddLines(lines, lineEnding); 41 | } 42 | 43 | public override IniToken CreateClone() 44 | { 45 | var clone = base.CreateClone() as MultilineToken; 46 | clone.Lines.AddRange(Lines); 47 | return clone; 48 | } 49 | 50 | public override void Write(ConfigIniWriter writer) 51 | { 52 | foreach (var line in Lines) 53 | { 54 | if (!line.IsNull) 55 | { 56 | writer.Write(line.Content); 57 | line.LineEnding.WriteTo(writer); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /UE4Config/Parsing/TextLine.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Parsing 2 | { 3 | public struct TextLine 4 | { 5 | public TextLine(string content = null, LineEnding lineEnding = LineEnding.None) 6 | { 7 | Content = content; 8 | LineEnding = lineEnding; 9 | } 10 | 11 | public string Content; 12 | public LineEnding LineEnding; 13 | 14 | public bool IsNull => Content == null; 15 | 16 | public override string ToString() 17 | { 18 | return Content + LineEnding.AsString(); 19 | } 20 | 21 | public static implicit operator TextLine(string content) 22 | { 23 | return new TextLine(content); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UE4Config/Parsing/TextToken.cs: -------------------------------------------------------------------------------- 1 | namespace UE4Config.Parsing 2 | { 3 | /// 4 | /// Token representing a pure text line 5 | /// 6 | public class TextToken : LineToken 7 | { 8 | public string Text {get;set;} 9 | 10 | public override void Write(ConfigIniWriter writer) 11 | { 12 | writer.Write(Text); 13 | LineEnding.WriteTo(writer); 14 | } 15 | 16 | public override IniToken CreateClone() 17 | { 18 | var clone = base.CreateClone() as TextToken; 19 | clone.Text = Text; 20 | return clone; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UE4Config/Parsing/WhitespaceToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UE4Config.Parsing 5 | { 6 | /// 7 | /// Describes whitespace characters found within the INI file that are neither part of an instruction nor comment 8 | /// 9 | public class WhitespaceToken : MultilineToken 10 | { 11 | public WhitespaceToken() : base() { } 12 | 13 | public WhitespaceToken(IEnumerable lines, LineEnding lineEnding) : base(lines, lineEnding) { } 14 | 15 | /// 16 | /// Condenses the whitespace to a single empty line 17 | /// 18 | public void Condense() 19 | { 20 | //Look for any lineEnding to take over 21 | LineEnding condensedLineEnding = LineEnding.None; 22 | foreach (var textLine in Lines) 23 | { 24 | if (textLine.LineEnding != LineEnding.None) 25 | { 26 | condensedLineEnding = textLine.LineEnding; 27 | } 28 | } 29 | if (Lines.Count > 0) 30 | { 31 | Lines.Clear(); 32 | AddLine(String.Empty, condensedLineEnding); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UE4Config/UE4Config.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Patrick Michael Hopf 6 | Infrablack 7 | UE4Config 8 | A straightlaced C# libary to edit Unreal Engine 4 config files, for UE4 projects and built games. 9 | Patrick Michael Hopf (C) 2020 10 | MIT 11 | https://github.com/Wortex17/UE4Config 12 | https://github.com/Wortex17/UE4Config 13 | git 14 | 0.0.0 15 | 0.0.0 16 | 0.0.0 17 | UE4Config 18 | UE4Config 19 | Infrablack.UE4Config 20 | True 21 | True 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | configuration: Release 3 | 4 | init: 5 | - cmd: "set ProductBaseVersion=0.7.2" 6 | - cmd: "echo ProductBaseVersion=%ProductBaseVersion%" 7 | - cmd: "set ProductVersion=%ProductBaseVersion%.%APPVEYOR_BUILD_NUMBER%" 8 | - cmd: "echo ProductVersion=%ProductVersion%" 9 | - cmd: "set ProductLongVersion=%ProductVersion%-%APPVEYOR_REPO_BRANCH%" 10 | - cmd: "echo ProductLongVersion=%ProductLongVersion%" 11 | - cmd: appveyor UpdateBuild -Version "%ProductVersion%" 12 | 13 | dotnet_csproj: 14 | patch: true 15 | file: '**\UE4Config*\*.csproj' 16 | version: '{version}' 17 | package_version: '{version}' 18 | assembly_version: '{version}' 19 | file_version: '{version}' 20 | informational_version: '$(ProductLongVersion)' 21 | 22 | before_build: 23 | - nuget restore 24 | - choco install opencover.portable 25 | - choco install codecov 26 | 27 | test_script: 28 | - OpenCover.Console.exe -register:administrator -target:"nunit3-console.exe" -targetargs:".\UE4Config.Tests\bin\Release\UE4Config.Tests.dll --result=myresults.xml;format=AppVeyor" -filter:"+[UE4Config*]* -[UE4Config.Tests*]*" -output:".\UE4Config_coverage.xml" 29 | - codecov -f "UE4Config_coverage.xml" 30 | 31 | build: 32 | publish_nuget: true 33 | 34 | artifacts: 35 | - path: 'UE4Config\bin\Release' 36 | name: UE4Config 37 | 38 | deploy: 39 | tag: $(APPVEYOR_REPO_TAG_NAME) # update the tag triggering this release deployment 40 | release: 'UE4Config $(ProductVersion)' 41 | description: 'Release for version $(ProductLongVersion)' 42 | draft: true 43 | provider: GitHub 44 | auth_token: $(GITHUB_RELEASE_TOKEN_ENCRYPTED) # your encrypted token from GitHub 45 | artifact: /.*\.nupkg/ # upload all NuGet packages to release assets 46 | on: 47 | APPVEYOR_REPO_TAG: true # deploy on tag push only 48 | --------------------------------------------------------------------------------