Paradox.Modding.Core.psm1
function ConvertTo-ParadoxArray { <# .SYNOPSIS Converts an array into the paradox-config equivalent. .DESCRIPTION Converts an array into the paradox-config equivalent. .PARAMETER Data The entries in the array to convert .PARAMETER Indentation What indentation level to maintain within the rows. .EXAMPLE PS C:\> ConvertTo-ParadoxArray -Data $entries -Indentation 5 Converts the provided array into the paradox-config equivalent with 5 tabs as indentation (6 for the individual entries). #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object[]] $Data, [int] $Indentation ) process { $results = do { '{' foreach ($line in $Data) { "$("`t" * ($Indentation + 1))$line"} "$("`t" * $Indentation)}" } while ($false) $results -join "`n" } } function ConvertTo-ParadoxConfig { <# .SYNOPSIS Builds a full Paradox Configuration file from hashtable. .DESCRIPTION Builds a full Paradox Configuration file from hashtable. The hashtable must have the same format as the file in the Paradox format is supposed to have. Tip: Use [ordered] hashtables in order to preserve order of entries. .PARAMETER Data The dat asets to convert into the target format. .PARAMETER Indentation What indentation level to start at. Will automatically increment for nested settings. Uses tabs for indentation. .PARAMETER TopLevel Whether the object provided is a top-levvel object. Top-Level Objects will not use opening and closing curly braces. They also skip the auto-indentation for direct members. .EXAMPLE PS C:\> $decisions | ConvertTo-ParadoxConfig Converts the decisions provided into valid mod strings. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ $_ -as [hashtable]})] [object[]] $Data, [int] $Indentation, [switch] $TopLevel ) begin { $boolMap = @{ $true = 'yes' $false = 'no' } $effectiveIndentation = $Indentation if (-not $TopLevel) { $effectiveIndentation++ } } process { foreach ($entry in $Data) { $results = do { if (-not $TopLevel) { "{" } foreach ($pair in $entry.GetEnumerator()) { if ($null -eq $pair.Value) { throw "Error processing $($pair.Key): NULL is not a supported value!" } $value = switch ($pair.Value.GetType()) { ([string]) { $pair.Value } ([int]) { $pair.Value } ([double]) { $pair.Value } ([bool]) { $boolMap[$pair.Value] } ([hashtable]) { ConvertTo-ParadoxConfig -Data $pair.Value -Indentation $effectiveIndentation; break } ([ordered]) { ConvertTo-ParadoxConfig -Data $pair.Value -Indentation $effectiveIndentation; break } ([object[]]) { ConvertTo-ParadoxArray -Data $pair.Value -Indentation $effectiveIndentation } default { throw "Error processing $($pair.Key): Unexpected type $($pair.Value.GetType().FullName) | $($pair.Value)" } } # Paradox Config can use the same key multiple times, hashtables cannot. # To solve this, we can append a "þ<number>þ" suffix in the hashtable which is removed during conversion "$("`t" * $effectiveIndentation)$($pair.Key -replace 'þ\d+þ') = $value" } if (-not $TopLevel) { "$("`t" * $Indentation)}" } } while ($false) $results -join "`n" } } } function Build-PdxMod { <# .SYNOPSIS Builds a a paradox game mod. .DESCRIPTION Builds a a paradox game mod. It will pick up all folders in the target Path with a matching name and treat them as mods. Then it builds & deploys them. This includes: - Copying the entire folder to a staging path. - Executing the build.ps1 script in the root folder (if present) - Apply any build extensions - Removing all PowerShell files from the mod structure - Replacing the mod with any previous versions in the destination path. .PARAMETER Path Directory where mod sources are looked for. Defaults to the current folder. .PARAMETER Name Name of the mod to build. Defaults to * .PARAMETER Tags Build extensions to apply based on their tags. These are provided by game-specific modules. This allows you to automatically apply game-specific build actions, without needing your own build script. .PARAMETER Game The game to build for. This is used to automatically pick the output folder for where that game is looking for mods. .PARAMETER OutPath The destination folder where to place the built mod. Use this if you do not want to deploy the mods straight to your game. .PARAMETER BadExtensions File extensions that should not remain in your mod structure. Before finalizing and deploying the mod, all files with one of the included extensions will be deleted from the staging copy of your mods. Defaults to: .ps1, .psd1 Note: This will NOT affect the files in your source structure, only in the copy used to wrap up and deploy your mod. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Build-PdxMod -Path C:\code\mods\Stellaris -Game Stellaris Picks up all folders under "C:\code\mods\Stellaris" and deploys them as Stellaris mod to where your Stellaris will look for mods. #> [CmdletBinding(DefaultParameterSetName = 'Game')] param ( [string] $Path = '.', [PsfArgumentCompleter('Paradox.Modding.ChildFolder')] [string] $Name = '*', [PsfArgumentCompleter('Paradox.Modding.BuildExtension.Tags')] [string[]] $Tags, [Parameter(Mandatory = $true, ParameterSetName = 'Game')] [ValidateSet('EU4', 'HOI4', 'Imperator', 'CK2', 'CK3', 'Victoria3', 'Stellaris')] [string] $Game, [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $OutPath, [string[]] $BadExtensions = @('.ps1','.psd1'), [switch] $EnableException ) begin { #region Utility Functions function Build-ModInstance { [CmdletBinding()] param ( [string] $Path, [string] $Name, [AllowEmptyCollection()] [string[]] $Tags, [AllowEmptyCollection()] [string[]] $BadExtensions ) # Prepare Configuration $rootPath = Join-Path -Path $Path -ChildPath $Name $modCfg = @{ Author = 'unspecified' Version = 'unspecified' } $configPath = Join-Path -Path $rootPath -ChildPath 'config.psd1' if (Test-Path -Path $configPath) { $config = Import-PSFPowerShellDataFile -Path $configPath foreach ($entry in $config.GetEnumerator()) { $modCfg[$entry.Key] = $entry.Value } } $modCfg.Name = $Name $modCfg.Root = $rootPath # Execute build script if present $buildPath = Join-Path -Path $rootPath -ChildPath 'build.ps1' if (Test-Path -Path $buildPath) { & $buildPath } # Apply Build Extensions if ($Tags) { $extensions = Get-PdxBuildExtension -Tags $Tags foreach ($extension in $extensions) { Write-PSFMessage -Level Verbose -Message ' Applying build Extension: {0}' -StringValues $extension.Name & $extension.Code $modCfg } } # Remove all PowerShell-native content from staging folder Get-ChildItem -Path $rootPath -Recurse | Where-Object Extension -In $BadExtensions | Remove-Item } function Deploy-ModInstance { [CmdletBinding()] param ( [string] $Path, [string] $Name, [string] $Destination ) $sourceRoot = Join-Path -Path $Path -ChildPath $Name $destinationRoot = Join-Path -Path $Destination -ChildPath $Name if (Test-Path -Path $destinationRoot) { Remove-Item -Path $destinationRoot -Recurse -Force } Move-Item -Path $sourceRoot -Destination $Destination -Force } #endregion Utility Functions if ($Game) { switch ($Game) { 'CK3' { $OutPath = "$([System.Environment]::GetFolderPath("MyDocuments"))\Paradox Interactive\Crusader Kings III\mod" } 'EU4' { $OutPath = "$([System.Environment]::GetFolderPath("MyDocuments"))\Paradox Interactive\Europa Universalis IV\mod" } 'HOI4' { $OutPath = "$([System.Environment]::GetFolderPath("MyDocuments"))\Paradox Interactive\Hearts of Iron IV\mod" } 'Imperator' { $OutPath = "$([System.Environment]::GetFolderPath("MyDocuments"))\Paradox Interactive\Imperator\mod" } 'Stellaris' { $OutPath = "$([System.Environment]::GetFolderPath("MyDocuments"))\Paradox Interactive\Stellaris\mod" } default { Stop-PSFFunction -Message "Game $Game not implemented yet! Use '-OutPath' instead to manually pick the deployment path!" -Cmdlet $PSCmdlet -EnableException $true } } } $tempDirectory = New-PSFTempDirectory -ModuleName PDX -Name Staging } process { Write-PSFMessage -Level Host -Message "Building Mods in '$Path' to '$OutPath'" foreach ($modRoot in Get-ChildItem -Path $Path -Directory) { if ($modRoot.Name -notlike $Name) { continue } if ($modRoot.Name -eq '.vscode') { continue } try { Write-PSFMessage -Level Host -Message " Processing: {0}" -StringValues $modRoot.Name -Target $modRoot.Name Write-PSFMessage -Level Host -Message " Staging Mod" -Target $modRoot.Name Copy-Item -LiteralPath $modRoot.FullName -Destination $tempDirectory -Recurse Write-PSFMessage -Level Host -Message " Building Mod. Tags: {0}" -StringValues ($Tags -join ',') -Target $modRoot.Name Build-ModInstance -Path $tempDirectory -Name $modRoot.Name -Tags $Tags -BadExtensions $BadExtensions Write-PSFMessage -Level Host -Message " Deploying Mod" -Target $modRoot.Name Deploy-ModInstance -Path $tempDirectory -Name $modRoot.Name -Destination $OutPath } catch { Stop-PSFFunction -Message "Failed to build $($modRoot.Name)" -ErrorRecord $_ -EnableException $EnableException -Continue -Cmdlet $PSCmdlet -Target $modRoot.Name } } } end { Get-PSFTempItem -ModuleName PDX | Remove-PSFTempItem } } function ConvertTo-PdxConfigFormat { <# .SYNOPSIS Builds a full Paradox Configuration file text from hashtable. .DESCRIPTION Builds a full Paradox Configuration file text from hashtable. The hashtable must have the same format as the file in the Paradox format is supposed to have. Tip: Use [ordered] hashtables in order to preserve order of entries. Duplicate Keys: In the Paradox format, there are often multiple entries with the same name. E.g.: Decisions with three options: Each option starts on the saem "option" key. Hashtables in c#/PowerShell/.NET do not support duplicate keys!! In those situations, you can append an index behind the key, that will automatically be removed during conversion: "optionþ1þ" becomes "option" "optionþ2þ" becomes "option" "optionþ10þ" becomes "option" The "þ" character is the "Thorn" symbol from the icelandic language and should never conflict with an actual key value. It can be typed by keeping the left ALT key pressed and type "0254" on the numpad. .PARAMETER Data The data assets to convert into the target format. .PARAMETER Indentation What indentation level to start at. Will automatically increment for nested settings. Uses tabs for indentation. .PARAMETER TopLevel Whether the object provided is a top-levvel object. Top-Level Objects will not use opening and closing curly braces. They also skip the auto-indentation for direct members. .EXAMPLE PS C:\> $decisions | ConvertTo-PdxConfigFormat Converts the decisions provided into valid mod strings. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ $_ -as [hashtable]})] [object[]] $Data, [int] $Indentation, [switch] $TopLevel ) begin { $boolMap = @{ $true = 'yes' $false = 'no' } $effectiveIndentation = $Indentation if (-not $TopLevel) { $effectiveIndentation++ } } process { foreach ($entry in $Data) { $results = do { if (-not $TopLevel) { "{" } foreach ($pair in $entry.GetEnumerator()) { if ($null -eq $pair.Value) { throw "Error processing $($pair.Key): NULL is not a supported value!" } $value = switch ($pair.Value.GetType()) { ([string]) { $pair.Value } ([int]) { $pair.Value } ([double]) { $pair.Value } ([bool]) { $boolMap[$pair.Value] } ([hashtable]) { ConvertTo-ParadoxConfig -Data $pair.Value -Indentation $effectiveIndentation; break } ([ordered]) { ConvertTo-ParadoxConfig -Data $pair.Value -Indentation $effectiveIndentation; break } ([object[]]) { ConvertTo-ParadoxArray -Data $pair.Value -Indentation $effectiveIndentation } default { Stop-PSFFunction -Message "Error processing $($pair.Key): Unexpected type $($pair.Value.GetType().FullName) | $($pair.Value)" -EnableException $true -Cmdlet $PSCmdlet } } # Paradox Config can use the same key multiple times, hashtables cannot. # To solve this, we can append a "þ<number>þ" suffix in the hashtable which is removed during conversion "$("`t" * $effectiveIndentation)$($pair.Key -replace 'þ\d+þ') = $value" } if (-not $TopLevel) { "$("`t" * $Indentation)}" } } while ($false) $results -join "`n" } } } function New-PdxConfigEntry { <# .SYNOPSIS Create a configuration entry, later to be written to disk as a Paradox mod file. .DESCRIPTION Create a configuration entry, later to be written to disk as a Paradox mod file. This command should rarely be used directly and is meant for the other modules in the kit as a helper utility. It is part of the process converting a psd1-based configuration file into actual mod files. The results of this command are later processed by ... - "Export-PdxLocalizedString" for localization data - "ConvertTo-PdxConfigFormat" for export as a Paradox-compliant mod file Parameter Note: IDictionary Most parameters accept a System.Collections.IDictionary as input. In the context of PowerShell, this usually means one of two options: - A Hashtable: $output = @{} - An Ordered Dictionary: $output = [ordered]@{} The latter has the advantage of maintaining the correct order of properties and is recommended for modding with this tool. For an example implementation / usage, see here: https://github.com/FriedrichWeinmann/Paradox.Modding.Stellaris/blob/master/Paradox.Modding.Stellaris/functions/ConvertTo-PdsBuilding.ps1 .PARAMETER Entry The individual configuration entry with the properties that make up the actual dataset. Whether that is a building definition, a new army type, and edict or whatever else. .PARAMETER Name The name of the entry. Used for localization keys. .PARAMETER Defaults Default settings that apply, if there is nothing specified in the Entry for it. .PARAMETER Output The result object. All processing is applied to this item. Specifying it allows you to pre-process it, adding the first entries yourself, before passing it to this command, which will affect the order of entries on the exported result. Defaults to: An empty ordered dictionary. .PARAMETER Common Common properties and their aliases. Entries specified here will be first - and in the order of its entries - in the exported mod data. It allows you to ensure a fairly consistent order for commonly needed properties. It also allows you to add aliases to some of the more common properties. Example: @{ CanBuild = 'can_build' Potential = 'potential' } The first entry allows you to either specify "CanBuild" or "can_build" in the config file - they'll mean the same thing. The second entry ensures the casing of "Potential" in the exported mod file will always be "potential", no matter how it is specified in the config file. It will also ensure, that in all exported entries, "can_build" comes before "potential" and all other entries will be below them. .PARAMETER Strings The localization data that will later be exported. Use "New-PdxLocalizedString" to generate this item. This allows you to collect localized strings from multiple entries and only generate one localization file at the end of the process. For example, this allows your config file with ten buildings defined to only generate one set of localization files, containing all the strings of the ten buildings bundled together. .PARAMETER LocalizationProperties Which properties on the Entry correspond to localization entries and not mod dataset information. They will NOT become part of the mod data, and instead become part of the localization. Example: @{ Name = 'edict_{0}' Description = 'edict_{0}_desc' } This would mean that for each entry, the configuration entries "Name" and "Description" will not become part of the mod, and instead become localization. If now our entire configuration entry be this: infernal_diplomacy = @{ Name = "Infernal Diplomacy" Description = "Who wouldn't want to be your friend?" Modifier = @{ envoys_add = 1 } } Then this tool will generate two strings: infernal_diplomacy:0 "Infernal Diplomacy" infernal_diplomacy_desc:0 "Who wouldn't want to be your friend?" .PARAMETER TypeMap Given the way PowerShell works, sometimes types are not exactly straightforward. With this parameter we can define a specific data type for a setting on an entry. Example: @{ upgrades = 'array' prerequisites = 'array' } If our configuration entry now specifies this: prerequisites = 'tech_basic_science_lab_2' Which is a plain string, it will then be converted into an array (with a single string) instead: prerequisites = @('tech_basic_science_lab_2') .PARAMETER Ignore Properties on Entry that should be ignored. Use this for properties you process outside of this command. For example, to simplify resource cost processing, which can be simple or incredibly complex, depending on how many conditions you want to apply or what kinds of resources you want to demand. Using Stellaris buildings for an example: - Most buildings cost Minerals to build (and nothing else). - Some buildings require rare resources as well. - Some buildings require rare resources, UNLESS a specific requirement is met. To make this convenient to mod, we want then support different specifications: - Provide a simple number, and we assume you mean minerals - Provide a simple hashtable, and we assume you provide your own calculation However, this command is not equipped to deal with this! So, in order to resolve this, you do that determination yourself and have this command skip the property instead. .EXAMPLE PS C:\> New-PdxConfigEntry -Entry $buildingData -Name $buildingName -Defaults $data.Core -Common $commonProps -Output $newBuilding -Strings $strings -LocalizationProperties $localeMap -Ignore Cost, Upkeep -TypeMap $typeMap Does wild stuff. Essentially, this merges the config entry's settings with the default settings, ensures strings are properly added to the list of strings to export, types are properly enforced and Cost and Upkeep properties are skipped. .LINK https://github.com/FriedrichWeinmann/Paradox.Modding.Stellaris/blob/master/Paradox.Modding.Stellaris/functions/ConvertTo-PdsBuilding.ps1 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([System.Collections.IDictionary])] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true)] [System.Collections.IDictionary] $Entry, [Parameter(Mandatory = $true, ParameterSetName = 'Localized')] [string] $Name, [System.Collections.IDictionary] $Defaults = @{}, [System.Collections.IDictionary] $Output = ([ordered]@{ }), [System.Collections.IDictionary] $Common = @{ }, [Parameter(Mandatory = $true, ParameterSetName = 'Localized')] [hashtable] $Strings, [Parameter(Mandatory = $true, ParameterSetName = 'Localized')] [hashtable] $LocalizationProperties, [hashtable] $TypeMap = @{}, [string[]] $Ignore ) begin { $nonCommonDefaults = @{ } foreach ($pair in $Defaults.GetEnumerator()) { if ($pair.Key -notin $Common.Keys) { $nonCommonDefaults[$pair.Key] = $pair.Value } } $propsToIgnore = @($Ignore) + $LocalizationProperties.Keys | Remove-PSFNull } process { #region Localization if ($Name) { foreach ($localizationEntry in $LocalizationProperties.GetEnumerator()) { if ($Entry.$($localizationEntry.Key) -is [string]) { Add-PdxLocalizedString -Key ($localizationEntry.Value -f $Name) -Text $Entry.$($localizationEntry.Key) -Strings $Strings } elseif ($Entry.$($localizationEntry.Key).english) { Add-PdxLocalizedString -Key ($localizationEntry.Value -f $Name) -Text $Entry.$($localizationEntry.Key).english -Localized $Entry.$($localizationEntry.Key) -Strings $Strings } } } #endregion Localization #region Well-Known Properties foreach ($property in $Common.GetEnumerator()) { if ($Entry.Keys -contains $property.Key) { $Output[$property.Value] = $Entry[$property.Key] } elseif ($Entry.Keys -contains $property.Value) { $Output[$property.Value] = $Entry[$property.Value] } elseif ($Defaults.Keys -contains $property.Key) { $Output[$property.Value] = $Defaults[$property.Key] } elseif ($Defaults.Keys -contains $property.Value) { $Output[$property.Value] = $Defaults[$property.Value] } } #endregion Well-Known Properties #region Other Nodes foreach ($pair in $Entry.GetEnumerator()) { if ($pair.Key -in $propsToIgnore) { continue } # Should Ignore if ($pair.Key -in $Common.Keys) { continue } # Handled in the last step $Output[$pair.Key] = $pair.Value } foreach ($pair in $nonCommonDefaults.GetEnumerator()) { if ($pair.Key -in $propsToIgnore) { continue } if ($pair.Key -in $Entry.Keys) { continue } $Output[$pair.Key] = $pair.Value } #endregion Other Nodes #region Process Type Conversion/Assurance foreach ($typeEntry in $TypeMap.GetEnumerator()) { if ($Output.Keys -notcontains $typeEntry.Key) { continue } switch ($typeEntry.Value) { 'array' { $Output[$typeEntry.Key] = @($Output[$typeEntry.Key]) } default { Write-PSFMessage -Level Warning -Message 'Unexpected type coercion type for property {0}. Defined type "{1}" is not implemented' -StringValues $typeEntry.Key, $typeEntry.Value -Target $Output } } } #endregion Process Type Conversion/Assurance $Output } } function Get-PdxBuildExtension { <# .SYNOPSIS Lists registered paradox modding build extensions. .DESCRIPTION Lists registered paradox modding build extensions. These are used to help simplify mod build scripts and usually provided by game-specific modules. .PARAMETER Name Name of the extension to look for. Defaults to * .PARAMETER Tags Tags to search for. Any extension with at least one match is returned. .EXAMPLE PS C:\> Get-PdxBuildExtension Lists all registered paradox modding build extensions. #> [CmdletBinding()] param ( [PsfArgumentCompleter('Paradox.Modding.BuildExtension.Name')] [string] $Name = '*', [PsfArgumentCompleter('Paradox.Modding.BuildExtension.Tags')] [string[]] $Tags ) process { $script:buildExtensions.Values | Where-Object { $_.Name -like $Name -and ( -not $Tags -or (@($_.Tags).Where{ $_ -in $Tags }) ) } } } function Register-PdxBuildExtension { <# .SYNOPSIS Register a new build logic specific to a given paradox game. .DESCRIPTION Register a new build logic specific to a given paradox game. These can be applied / selected when building mods to simplify/replace build scripts in mods. .PARAMETER Name Name of the build extension. .PARAMETER Description Description of what the build extension does. .PARAMETER Tags Tags to apply to the extension. Extensions are usually selected by tag. Each extension should at least include a tag for the game it applies to. .PARAMETER Code The code implementing the build logic. It will receive a single value as input: A hashtable with multiple entrie. As a least minimum, this will include: - Root: The Path to the mod being built - Name: The name of the mod - Author: The author of the mod - Version: The version of the mod Other than "Root" or "Name", data is taken from a "config.psd" file in the root mod folder if present. Individual games may define (and use) additional config settings as desired. .EXAMPLE PS C:\> Register-PdxBuildExtension -Name 'Stellaris.Edict' -Description 'Builds Stellaris edicts written as psd1 files' -Tags stellaris, edicts -Code $edictExt Registers a new build extension to simplify building Stellaris edicts. #> [CmdletBinding()] param ( [PsfValidateScript('PSFramework.Validate.SafeName')] [string] $Name, [string] $Description, [string[]] $Tags, [scriptblock] $Code ) process { $script:buildExtensions[$Name] = [PSCustomObject]@{ PSTypeName = 'Paradox.Modding.Core.BuildExtension' Name = $Name Description = $Description Tags = $Tags Code = $Code } } } function Read-PdxFileSection { <# .SYNOPSIS Read a paradox game file and break it into individual entries. .DESCRIPTION Read a paradox game file and break it into individual entries. Use this to parse out individual entries in a file containing many elements. E.g.: Use this to get a list of all techs from all the files in the "common/technologies" folder (including the individual definitions). .PARAMETER Path Path to the files to parse. .PARAMETER IncludeComments Also include comments _within_ an individual entry. by default, commented out lines are not included. Comments at the root level will always be ignored. .EXAMPLE PS C:\> Get-ChildItem -Path .\common\technologies | Read-PdxFileSection Read all technologies from all tech files. #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [PSFFileLax] $Path, [switch] $IncludeComments ) process { foreach ($badFile in $Path.FailedInput) { Write-PSFMessage -Level Warning -Message "Bad input - either does not exist or not a file: {0}" -StringValues $badFile } foreach ($file in $Path) { $lines = Get-Content -LiteralPath $file | Where-Object { $IncludeComments -or $_ -notmatch '^\s{0,}#' } $currentLines = @() foreach ($line in $lines) { # Skip empty lines outside of a section if (-not $line.Trim() -and -not $currentLines) { continue } # Skip leading declarations if (-not $currentLines -and $line -notmatch '=\s{0,}\{' -and $line -notmatch '^#') { continue } $currentLines += $line # Move to next line if not last line if ($line -notmatch '^}') { continue } [PSCustomObject]@{ File = $file Name = $currentLines[0] -replace '.{0,}?(\w+)\s{0,}=.{0,}', '$1' Lines = $currentLines LinesCompact = $currentLines | Get-SubString -Trim "`t " Text = $currentLines -join "`n" } $currentLines = @() } } } } function Update-PdxFileSection { <# .SYNOPSIS Modifies the content of a section read from a Paradox content file. .DESCRIPTION Modifies the content of a section read from a Paradox content file. Use this to modify the sections read through "Read-PdxFileSection". IMPORTANT: This does _NOT_ modify the files those were read from - you'll have to write back the sections yourself (or put them in new files). .PARAMETER Section The section data to modify. Use "Read-PdxFileSection" to generate them. .PARAMETER Rule The rules to apply to each section. Currently supported rule-types: - Insert: Adds extra content above or below existing lines. - Replace: Use regex replacement on the entire section. Examples: > Insert: @{ Type = "Insert" Scope = 'Above' Line = 'station_modifier = {' Text = '# Will turn the base into a juggernaut' } Notes: The "Text"-Field can have any number of lines of text, as needed. > Replace: @{ Type = 'Replace' Old = 'max = 320 # 20 \* 16' New = 'max = 3200 # 200 * 16' } Notes: This replacement uses regular expressions. Mind your special characters. It applies the ruleset as used by C#. .PARAMETER PassThru Return the processed section object. By default, this command returns nothing, merely modifying the sections passed to it. .EXAMPLE PS C:\> Update-PdxFileSection -Section $allComponents -Rule $prerequisites Updates all the sections in $allComponents based on the rules in $prerequisites. - See description on "-Rule" parameter to see how rules should be defined. - See "Read-PdxFileSection" command for how to obtain sections. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]] $Section, [hashtable[]] $Rule, [switch] $PassThru ) process { foreach ($entry in $Section) { :rules foreach ($ruleItem in $Rule) { switch ($ruleItem.Type) { 'Insert' { $index = $entry.Lines.IndexOf($ruleItem.Line) if ($index -lt 0) { $index = $entry.LinesCompact.IndexOf($ruleItem.Line) } if ($index -lt 0) { Write-PSFMessage -Level Warning -Message "Error updating {0}: Line not found: {1}" -StringValues $entry.Name, $ruleItem.Line -Target $entry continue rules } $start = $index if ('Above' -eq $ruleItem.Scope) { $start = $start - 1 } $newLines = $entry.Lines[0..$start] + ($ruleItem.Text -split "[`n`r]+") + $entry.Lines[($start + 1)..($entry.Lines.Count - 1)] $entry.Lines = $newLines $entry.LinesCompact = $newLines | Get-SubString -Trim "`t " $entry.Text = $NewLines -join "`n" } 'Replace' { if ($ruleItem.FullText) { $newText = $entry.Text -replace $ruleItem.Old, $ruleItem.New $newLines = $newText -split '[\n\r]+' } else { $newLines = $entry.Lines -replace $ruleItem.Old, $ruleItem.New } $entry.Lines = $newLines $entry.LinesCompact = $newLines | Get-SubString -Trim "`t " $entry.Text = $NewLines -join "`n" } default { Write-PSFMessage -Level Warning -Message 'Error processing rule: Unexpected rule type: {0}' -StringValues $ruleItem.Type -Data @{ Rule = $ruleItem } -Target $entry } } } if ($PassThru) { $entry } } } } function Add-PdxLocalizedString { <# .SYNOPSIS Adds a localization entry into a previously created localization hashtable. .DESCRIPTION Adds a localization entry into a previously created localization hashtable. This allows defining a default text for languages otherwise not defined (or just simply going mono-lingual). .PARAMETER Key The string key to provide text for. .PARAMETER Text The default text to apply. .PARAMETER Localized Any additional string mappings for different languages. .PARAMETER Strings The central strings dictionary. Use New-PdxLocalizedString to generate one. .EXAMPLE PS C:\> Add-PdxLocalizedString -Key $traitLocName -Text 'Suicide Pact' -Localized $trait.Localized -Strings $strings Adds the string in $traitLocName with a default text of "Suicide Pact" and whatever we cared to localize to the $strings dictionary #> [CmdletBinding()] param ( [string] $Key, [AllowEmptyString()] [AllowNull()] [string] $Text, [AllowNull()] [hashtable] $Localized, [hashtable] $Strings ) if (-not $Strings) { return } $languages = $Strings.Keys foreach ($language in $languages) { $Strings[$language][$Key] = $Text if (-not $Localized) { continue } if ($Localized[$language]) { $Strings[$language][$Key] = $Localized[$language] } } } function Export-PdxLocalizedString { <# .SYNOPSIS Creates language files from a localized strings hashtable. .DESCRIPTION Creates language files from a localized strings hashtable. Expects a hashtable with one nested hashtable per language, using the system language name as key. Create a new strings hashtable with New-PdxLocalizedString Add entries with Add-PdxLocalizedString .PARAMETER Strings A hashtable with one nested hashtable per language, using the system language name as key. .PARAMETER ModRoot The root path, where the mod begins. .PARAMETER Name The basic name of the strings file to be created. .PARAMETER ToLower Whether localization keys should be converted to lowercase .EXAMPLE PS C:\> Export-PdxLocalizedString -Strings $strings -ModRoot $resolvedPath -Name $Definition.Name Exports the localization stored in $strings to the correct place under the mod root, with the name provided. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $Strings, [Parameter(Mandatory = $true)] [string] $ModRoot, [Parameter(Mandatory = $true)] [string] $Name, [switch] $ToLower ) process { foreach ($language in $Strings.Keys) { $lines = @() foreach ($pair in $strings[$language].GetEnumerator()) { if ($ToLower) { $lines += ' {0}:0 "{1}"' -f $pair.Key.ToLower(), ($pair.Value -replace '"',"'") } else { $lines += ' {0}:0 "{1}"' -f $pair.Key, ($pair.Value -replace '"',"'") } } $lines = @("l_$($language):") + ($lines | Sort-Object) $localizedText = $lines -join "`n" $encoding = [System.Text.UTF8Encoding]::new($true) $outFolder = Join-Path -Path $ModRoot -ChildPath "localisation/$language" if (-not (Test-Path -Path $outFolder)) { $null = New-Item -Path $outFolder -ItemType Directory -Force } $outPath = Join-Path -Path $ModRoot -ChildPath "localisation/$language/$($Name)_l_$($language).yml" [System.IO.File]::WriteAllText($outPath, $localizedText, $encoding) } } } function New-PdxLocalizedString { <# .SYNOPSIS Creates a new set of localization data for a Paradox game mod. .DESCRIPTION Creates a new set of localization data for a Paradox game mod. This can be used to conveniently build up localization data as you generate mod content, and later export it as localization files. Mostly used to enable mods to keep content and localization together. Especially when you are not planning to localize anyway. Add entries with Add-PdxLocalizedString Export to disk with Export-PdxLocalizedString .EXAMPLE PS C:\> New-PdxLocalizedString Generates a hashtable, mapping all the supported languages. There really is nothing else to it. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([hashtable])] [CmdletBinding()] param () process { @{ braz_por = @{ } english = @{ } french = @{ } german = @{ } japanese = @{ } polish = @{ } russian = @{ } simp_chinese = @{ } spanish = @{ } } } } Register-PSFTeppScriptblock -Name 'Paradox.Modding.ChildFolder' -ScriptBlock { $root = '.' if ($fakeBoundParameter.Path) { $root = $fakeBoundParameter.Path } Get-ChildItem -Path $root -Directory | ForEach-Object { @{ Text = $_.Name; Tooltip = $_.FullName} } } -Global Register-PSFTeppScriptblock -Name 'Paradox.Modding.BuildExtension.Tags' -ScriptBlock { (Get-PdxBuildExtension).Tag | Write-Output | Sort-Object -Unique } -Global Register-PSFTeppScriptblock -Name 'Paradox.Modding.BuildExtension.Name' -ScriptBlock { (Get-PdxBuildExtension).Name | Sort-Object -Unique } -Global # Storage for Game-Specific build extensions. # These allow modules to extend the default build steps without requiring the module author to handle them in their build script (if any). $script:buildExtensions = @{} |