Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <#
- .SYNOPSIS
- Looks for conflicting mods for The Witcher 3 and displays them.
- .DESCRIPTION
- Builds a map of all files each mod adds and/or modifies and then checks if they overlap.
- Considered are scripts and every file listed within a .bundle file -- so everything.
- The default parameters are setup so that the script will exit if no conflict were found.
- And otherwise block and wait for user input, so it can display each conflict.
- .PARAMETER GamePath
- Path to The Witcher 3 installation directory.
- If that parameter is not specified, the script will try the following to resolve it:
- 1. Check the working directory
- 2. Check for the GOG version (via Windows Registry)
- 3. Prompts with a Folder-Picker dialog.
- .PARAMETER PrintReport
- Prints a human-readable report.
- Otherwise the build object structure is returned.
- .PARAMETER BlockOnConflict
- If specified and any mod conflict is detected, will prompt the user to press any key (waiting to exit).
- .EXAMPLE
- PS> DiscoverWitcher3ModConflicts
- .EXAMPLE
- PS> DiscoverWitcher3ModConflicts -GamePath 'G:\The Witcher 3'
- .NOTES
- Create a shortcut to run the script:
- 1. From Desktop/Explorer
- Right-click -> New -> Shortcut
- 2. For Location use:
- C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoE -File <ScriptPath>
- 3. Replace '<ScriptPath>' with the path to this script.
- If that path contains spaces enclose it in " (double-quotes).
- 4. Click 'Next', choose any name you like
- 5. Click 'Finish'
- Author:
- Xetrill <[email protected]>
- Version History:
- 1.0: Initial working version
- 1.1: Added GamePath parameter
- 1.2: Added documentation
- Fixed GamePath parameter type
- 1.3: Added BlockOnConflict parameter
- 1.4: Added a ParameterSet.
- 1.5: Replaced immuatable array's with List's, making the script run faster.
- #>
- [CmdletBinding(DefaultParameterSetName = 'Report')]
- param(
- [Parameter(Position = 0, Mandatory = $false)]
- [string]$GamePath,
- [Parameter(Position = 1, Mandatory = $false, ParameterSetName = 'Report')]
- [Switch]$PrintReport = $true,
- [Parameter(Position = 2, Mandatory = $false, ParameterSetName = 'Report')]
- [Switch]$BlockOnConflict = $true
- )
- function TestIsWitcherInstallDirectory
- {
- [CmdletBinding()]
- [OutputType([bool])]
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [string]$Path
- )
- return (
- (Test-Path "$Path\bin\x64\witcher3.exe" -PathType Leaf -ErrorAction SilentlyContinue)
- )
- }
- function ResolveWitcherInstallDirectory
- {
- [CmdletBinding()]
- param()
- # test user provided path first, if any
- if ($PSBoundParameters.ContainsKey('GamePath') -and
- (TestIsWitcherInstallDirectory -Path $GamePath)) {
- Write-Verbose "Game Path resolved by User: '$Path'"
- $Script:GameRoot = $GamePath
- return
- }
- # try working directory
- if (TestIsWitcherInstallDirectory -Path $PSScriptRoot) {
- Write-Verbose "Game Path resolved by Working Directory: '$PSScriptRoot'"
- $Script:GameRoot = $PSScriptRoot
- return
- }
- # try for the GOG version
- if (Test-Path -Path Registry::HKLM\SOFTWARE\Wow6432Node\GOG.com\Games\1207664643 -ErrorAction SilentlyContinue) {
- $Path = (Get-ItemProperty -Path Registry::HKLM\SOFTWARE\Wow6432Node\GOG.com\Games\1207664643 -Name Path).Path
- if (TestIsWitcherInstallDirectory -Path $Path) {
- Write-Verbose "Game Path resolved by Registry lookup (GOG): '$Path'"
- $Script:GameRoot = $Path
- return
- }
- }
- # finally, just ask the user
- try {
- [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
- $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog
- $Dialog.RootFolder = 'MyComputer'
- $Dialog.Description = 'Select the Witcher 3 install directory'
- $Dialog.ShowNewFolderButton = $false
- while (-not (Get-Variable GameRoot -Scope Script -ErrorAction SilentlyContinue)) {
- $DialogResult = $Dialog.ShowDialog()
- if ($DialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
- break
- }
- if ($DialogResult -eq [System.Windows.Forms.DialogResult]::OK) {
- if (TestIsWitcherInstallDirectory -Path $Dialog.SelectedPath) {
- Write-Verbose "Game Path resolved by User (Prompt): '$($Dialog.SelectedPath)'"
- $Script:GameRoot = $Dialog.SelectedPath
- return
- }
- else {
- $DialogResult = [System.Windows.Forms.MessageBox]::Show(
- @"
- This is not the Witcher 3 install directory.
- It must contain:
- - bin\x64\witcher3.exe
- "@,
- $PSCmdlet.MyInvocation.MyCommand.Name,
- [System.Windows.Forms.MessageBoxButtons]::OKCancel,
- [System.Windows.Forms.MessageBoxIcon]::Asterisk,
- [System.Windows.Forms.MessageBoxDefaultButton]::Button1
- )
- if ($DialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
- break
- }
- }
- }
- else {
- break
- }
- }
- }
- finally {
- if ($Dialog) {
- $Dialog.Dispose()
- }
- }
- if ((Get-Variable GameRoot -Scope Script -ErrorAction SilentlyContinue) -and
- (Test-Path -Path $Script:GameRoot -PathType Container -ErrorAction SilentlyContinue)) {
- return
- }
- Write-Error -Message 'Unable to detect Witcher 3 install directory.' -Category ObjectNotFound
- }
- function ExtractContentListFromBundle
- {
- [CmdletBinding()]
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [ValidateScript({ $_ | Test-Path -PathType Leaf })]
- [string]$FilePath
- )
- Write-Verbose " Reading Bundle: $FilePath"
- $ContentList = @()
- try
- {
- $Stream = New-Object System.IO.FileStream `
- -ArgumentList $FilePath, `
- ([System.IO.FileMode]::Open), `
- ([System.IO.FileAccess]::Read)
- $Reader = New-Object System.IO.BinaryReader `
- -ArgumentList $Stream, `
- ([System.Text.Encoding]::ASCII)
- if ([System.Text.ASCIIEncoding]::ASCII.GetString($Reader.ReadBytes(8)) -ne 'POTATO70') {
- Write-Error -Message 'Unknown file format.' -Category InvalidData -TargetObject $FilePath
- return
- }
- $Stream.Seek(8, [System.IO.SeekOrigin]::Current) | Out-Null
- $DataOffset = $Reader.ReadUInt32()
- $EntryCount = $DataOffset / 320 # entry-size
- Write-Verbose " DataOffest: $DataOffset"
- Write-Verbose " EntryCount: $EntryCount"
- $Stream.Seek(32, [System.IO.SeekOrigin]::Begin) | Out-Null
- $Buffer = New-Object System.Text.StringBuilder
- for ($i = 0; $i -lt $EntryCount; ++$i) {
- Write-Progress -Id 1 -ParentId 0 `
- -Activity 'Reading Bundle' `
- -Status ($FilePath.Substring($Script:GameRoot.Length + 1)) `
- -PercentComplete ($i / $EntryCount * 100)
- $Stream.Seek(32 + $i * 320, [System.IO.SeekOrigin]::Begin) | Out-Null
- $Char = $Reader.ReadChar()
- while ($Char -ne [char]0) {
- $Buffer.Append($Char) | Out-Null
- $Char = $Reader.ReadChar()
- }
- $ContentList += $Buffer.ToString()
- $Buffer.Length = 0
- }
- Write-Progress -Id 1 -ParentId 0 `
- -Activity 'Reading Bundle' `
- -Status ($FilePath.Substring($Script:GameRoot.Length + 1)) `
- -Completed
- }
- finally {
- if ($Stream) {
- $Stream.Close()
- }
- }
- return $ContentList
- }
- function BuildModPriorityList
- {
- [CmdletBinding()]
- param()
- $Script:ModPriorityList = New-Object -TypeName 'System.Collections.Generic.List[psobject]'
- foreach ($ModDir in (Get-ChildItem -Path "$Script:GameRoot\mods" -Directory -Include 'mod*')) {
- $Script:ModPriorityList.Add($ModDir.Name)
- }
- $Script:ModPriorityList = $Script:ModPriorityList | Sort-Object
- if ($PSBoundParameters['Verbose']) {
- Write-Verbose "Found $($Script:ModPriorityList.Length) Mods - Listing in Load Order:"
- for ($i = 0; $i -lt $Script:ModPriorityList.Length; ++$i) {
- Write-Verbose (' {0,2}: {1}' -f $i, $Script:ModPriorityList[$i])
- }
- }
- }
- function BuildModFileMap
- {
- [CmdletBinding()]
- param()
- $Script:ModFileMap = @{}
- $Script:ModFileMap | Add-Member -MemberType ScriptMethod -Name AddEntry -Value {
- param(
- [string]$File,
- [int]$ModIndex
- )
- if (-not ($this.ContainsKey($File))) {
- $this[$File] = New-Object -TypeName 'System.Collections.Generic.List[int]'
- }
- $this[$File].Add($ModIndex)
- }
- $NormalizedFile = ''
- for ($i = 0; $i -lt $Script:ModPriorityList.Count; ++$i) {
- $ModContentRoot = "$Script:GameRoot\mods\$($Script:ModPriorityList[$i])\content"
- Write-Progress -Id 0 -Activity 'Building mod file map' `
- -Status ($Script:ModPriorityList[$i]) `
- -PercentComplete ($i / $Script:ModPriorityList.Count * 100)
- Write-Verbose "Content From: $($Script:ModPriorityList[$i])"
- foreach ($BundleFile in (Get-ChildItem -Path $ModContentRoot -Include '*.bundle' `
- -Recurse -File -ErrorAction SilentlyContinue)) {
- foreach ($File in (ExtractContentListFromBundle -FilePath $BundleFile)) {
- $NormalizedFile = $File.ToLowerInvariant()
- Write-Verbose " Bundle: $NormalizedFile"
- $Script:ModFileMap.AddEntry($NormalizedFile, $i)
- }
- }
- $Path = "$ModContentRoot\scripts"
- if (Test-Path $Path -PathType Container -ErrorAction SilentlyContinue) {
- foreach ($File in (Get-ChildItem $Path -Include '*.ws' -Recurse -File)) {
- $NormalizedFile = $File.FullName.Substring($ModContentRoot.Length + 1).ToLowerInvariant()
- Write-Verbose " Script: $NormalizedFile"
- $Script:ModFileMap.AddEntry($NormalizedFile, $i)
- }
- }
- }
- Write-Progress -Id 0 -Activity 'Building mod file map' -Completed
- }
- function GetModFileConflicts
- {
- [CmdletBinding()]
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [string]$File
- )
- Write-Verbose "Testing File: $File"
- $ModList = New-Object -TypeName 'System.Collections.Generic.List[string]'
- if ($Script:ModFileMap[$File].Count -gt 1) {
- foreach ($index in $Script:ModFileMap[$File]) {
- $ModList.Add($Script:ModPriorityList[$index])
- }
- }
- return $ModList
- }
- function PrintSingleModConflictReport
- {
- [CmdletBinding()]
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [string]$File,
- [Parameter(Position = 0, Mandatory = $true)]
- [psobject[]]$ModList
- )
- Write-Host ' [' -ForegroundColor Gray -NoNewline
- Write-Host $File -ForegroundColor Cyan -NoNewline
- Write-Host '] [' -ForegroundColor Gray -NoNewline
- Write-Host $ModList.Length -NoNewline
- Write-Host ']' -ForegroundColor Gray
- for ($i = 0; $i -lt $ModList.Length; ++$i) {
- Write-Host ' ' -NoNewline
- if ($i -eq 0) {
- Write-Host $ModList[$i] -ForegroundColor Green
- }
- else {
- Write-Host $ModList[$i]
- }
- }
- }
- function PrintModConflictReport
- {
- [CmdletBinding()]
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [hashtable]$ConflictMap
- )
- Write-Host '---' -ForegroundColor Gray -NoNewline
- Write-Host ' MOD CONFLICT REPORT /' -NoNewline
- Write-Host '---------------------------------------------------------' `
- -ForegroundColor Gray
- Write-Host ' Mod Count: ' -NoNewline
- Write-Host ($Script:ModPriorityList.Count) -ForegroundColor Yellow
- Write-Host ' File Count: ' -NoNewline
- Write-Host ($Script:ModFileMap.Count) -ForegroundColor Yellow
- Write-Host ' Conflict Count: ' -NoNewline
- if ($ConflictMap.Count -eq 0) {
- Write-Host ($ConflictMap.Count) -ForegroundColor Green
- }
- else {
- Write-Host ($ConflictMap.Count) -ForegroundColor Red
- Write-Host ' Conflicts:'
- Write-Host
- foreach ($ModFile in $ConflictMap.Keys) {
- PrintSingleModConflictReport -File $ModFile -ModList $ConflictMap[$ModFile]
- Write-Host
- }
- }
- Write-Host '---------------------------------------------------------' `
- -ForegroundColor Gray -NoNewline
- Write-Host '/ MOD CONFLICT REPORT ' -NoNewline
- Write-Host '---' -ForegroundColor Gray
- }
- function GetAllModFileConflicts
- {
- [CmdletBinding()]
- param()
- $ConflictMap = @{}
- foreach ($ModFile in $Script:ModFileMap.Keys) {
- $ModList = GetModFileConflicts -File $ModFile
- if ($ModList.Length -gt 0) {
- $ConflictMap[$ModFile] = $ModList
- }
- }
- if ($PrintReport) {
- PrintModConflictReport -ConflictMap $ConflictMap
- if ($BlockOnConflict -and $ConflictMap.Count -ne 0) {
- Write-Host 'Press <Enter> to continue.'
- [System.Console]::ReadLine() | Out-Null
- }
- }
- else {
- return $ConflictMap
- }
- }
- #------------------------------------------------------------------------------
- Clear-Host
- ResolveWitcherInstallDirectory
- BuildModPriorityList
- BuildModFileMap
- GetAllModFileConflicts
Advertisement
Add Comment
Please, Sign In to add comment