Xetrill

DiscoverWitcher3ModConflicts.ps1

Sep 26th, 2015
389
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2. .SYNOPSIS
  3.     Looks for conflicting mods for The Witcher 3 and displays them.
  4.  
  5. .DESCRIPTION
  6.     Builds a map of all files each mod adds and/or modifies and then checks if they overlap.
  7.  
  8.     Considered are scripts and every file listed within a .bundle file -- so everything.
  9.  
  10.     The default parameters are setup so that the script will exit if no conflict were found.
  11.     And otherwise block and wait for user input, so it can display each conflict.
  12.  
  13. .PARAMETER GamePath
  14.     Path to The Witcher 3 installation directory.
  15.     If that parameter is not specified, the script will try the following to resolve it:
  16.         1. Check the working directory
  17.         2. Check for the GOG version (via Windows Registry)
  18.         3. Prompts with a Folder-Picker dialog.
  19.  
  20. .PARAMETER PrintReport
  21.     Prints a human-readable report.
  22.     Otherwise the build object structure is returned.
  23.  
  24. .PARAMETER BlockOnConflict
  25.     If specified and any mod conflict is detected, will prompt the user to press any key (waiting to exit).
  26.  
  27. .EXAMPLE
  28.     PS> DiscoverWitcher3ModConflicts
  29.  
  30. .EXAMPLE
  31.     PS> DiscoverWitcher3ModConflicts -GamePath 'G:\The Witcher 3'
  32.  
  33. .NOTES
  34.     Create a shortcut to run the script:
  35.         1. From Desktop/Explorer
  36.             Right-click -> New -> Shortcut
  37.         2. For Location use:
  38.             C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoE -File <ScriptPath>
  39.         3. Replace '<ScriptPath>' with the path to this script.
  40.             If that path contains spaces enclose it in " (double-quotes).
  41.         4. Click 'Next', choose any name you like
  42.         5. Click 'Finish'
  43.  
  44.     Author:
  45.         Xetrill <[email protected]>
  46.    
  47.     Version History:
  48.         1.0: Initial working version
  49.         1.1: Added GamePath parameter
  50.         1.2: Added documentation
  51.              Fixed GamePath parameter type
  52.         1.3: Added BlockOnConflict parameter
  53.         1.4: Added a ParameterSet.
  54.         1.5: Replaced immuatable array's with List's, making the script run faster.
  55. #>
  56. [CmdletBinding(DefaultParameterSetName = 'Report')]
  57. param(
  58.     [Parameter(Position = 0, Mandatory = $false)]
  59.     [string]$GamePath,
  60.  
  61.     [Parameter(Position = 1, Mandatory = $false, ParameterSetName = 'Report')]
  62.     [Switch]$PrintReport = $true,
  63.  
  64.     [Parameter(Position = 2, Mandatory = $false, ParameterSetName = 'Report')]
  65.     [Switch]$BlockOnConflict = $true
  66. )
  67.  
  68.  
  69. function TestIsWitcherInstallDirectory
  70. {
  71.     [CmdletBinding()]
  72.     [OutputType([bool])]
  73.     param(
  74.         [Parameter(Position = 0, Mandatory = $true)]
  75.         [string]$Path
  76.     )
  77.  
  78.     return (
  79.         (Test-Path "$Path\bin\x64\witcher3.exe" -PathType Leaf -ErrorAction SilentlyContinue)
  80.     )
  81. }
  82.  
  83.  
  84. function ResolveWitcherInstallDirectory
  85. {
  86.     [CmdletBinding()]
  87.     param()
  88.  
  89.     # test user provided path first, if any
  90.     if ($PSBoundParameters.ContainsKey('GamePath') -and
  91.         (TestIsWitcherInstallDirectory -Path $GamePath)) {
  92.         Write-Verbose "Game Path resolved by User: '$Path'"
  93.         $Script:GameRoot = $GamePath
  94.         return
  95.     }
  96.  
  97.     # try working directory
  98.     if (TestIsWitcherInstallDirectory -Path $PSScriptRoot) {
  99.         Write-Verbose "Game Path resolved by Working Directory: '$PSScriptRoot'"
  100.         $Script:GameRoot = $PSScriptRoot
  101.         return
  102.     }
  103.  
  104.     # try for the GOG version
  105.     if (Test-Path -Path Registry::HKLM\SOFTWARE\Wow6432Node\GOG.com\Games\1207664643 -ErrorAction SilentlyContinue) {
  106.         $Path = (Get-ItemProperty -Path Registry::HKLM\SOFTWARE\Wow6432Node\GOG.com\Games\1207664643 -Name Path).Path
  107.         if (TestIsWitcherInstallDirectory -Path $Path) {
  108.             Write-Verbose "Game Path resolved by Registry lookup (GOG): '$Path'"
  109.             $Script:GameRoot = $Path
  110.             return
  111.         }
  112.     }
  113.  
  114.     # finally, just ask the user
  115.     try {
  116.         [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
  117.  
  118.         $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog
  119.         $Dialog.RootFolder          = 'MyComputer'
  120.         $Dialog.Description         = 'Select the Witcher 3 install directory'
  121.         $Dialog.ShowNewFolderButton = $false
  122.  
  123.         while (-not (Get-Variable GameRoot -Scope Script -ErrorAction SilentlyContinue)) {
  124.             $DialogResult = $Dialog.ShowDialog()
  125.  
  126.             if ($DialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
  127.                 break
  128.             }
  129.  
  130.             if ($DialogResult -eq [System.Windows.Forms.DialogResult]::OK) {
  131.                 if (TestIsWitcherInstallDirectory -Path $Dialog.SelectedPath) {
  132.                     Write-Verbose "Game Path resolved by User (Prompt): '$($Dialog.SelectedPath)'"
  133.                     $Script:GameRoot = $Dialog.SelectedPath
  134.                     return
  135.                 }
  136.                 else {
  137.                     $DialogResult = [System.Windows.Forms.MessageBox]::Show(
  138.                         @"
  139. This is not the Witcher 3 install directory.
  140. It must contain:
  141.    - bin\x64\witcher3.exe
  142. "@,
  143.                         $PSCmdlet.MyInvocation.MyCommand.Name,
  144.                         [System.Windows.Forms.MessageBoxButtons]::OKCancel,
  145.                         [System.Windows.Forms.MessageBoxIcon]::Asterisk,
  146.                         [System.Windows.Forms.MessageBoxDefaultButton]::Button1
  147.                     )
  148.                     if ($DialogResult -eq [System.Windows.Forms.DialogResult]::Cancel) {
  149.                         break
  150.                     }
  151.                 }
  152.             }
  153.             else {
  154.                 break
  155.            }
  156.         }
  157.     }
  158.     finally {
  159.         if ($Dialog) {
  160.             $Dialog.Dispose()
  161.         }
  162.     }
  163.  
  164.     if ((Get-Variable GameRoot -Scope Script -ErrorAction SilentlyContinue) -and
  165.         (Test-Path -Path $Script:GameRoot -PathType Container -ErrorAction SilentlyContinue)) {
  166.         return
  167.     }
  168.     Write-Error -Message 'Unable to detect Witcher 3 install directory.' -Category ObjectNotFound
  169. }
  170.  
  171.  
  172. function ExtractContentListFromBundle
  173. {
  174.     [CmdletBinding()]
  175.     param(
  176.         [Parameter(Position = 0, Mandatory = $true)]
  177.         [ValidateScript({ $_ | Test-Path -PathType Leaf })]
  178.         [string]$FilePath
  179.     )
  180.     Write-Verbose "  Reading Bundle:  $FilePath"
  181.  
  182.     $ContentList = @()
  183.  
  184.     try
  185.     {
  186.         $Stream = New-Object System.IO.FileStream `
  187.             -ArgumentList $FilePath, `
  188.                           ([System.IO.FileMode]::Open), `
  189.                           ([System.IO.FileAccess]::Read)
  190.         $Reader = New-Object System.IO.BinaryReader `
  191.             -ArgumentList $Stream, `
  192.                           ([System.Text.Encoding]::ASCII)
  193.        
  194.         if ([System.Text.ASCIIEncoding]::ASCII.GetString($Reader.ReadBytes(8)) -ne 'POTATO70') {
  195.             Write-Error -Message 'Unknown file format.' -Category InvalidData -TargetObject $FilePath
  196.             return
  197.         }
  198.  
  199.         $Stream.Seek(8, [System.IO.SeekOrigin]::Current) | Out-Null
  200.        
  201.         $DataOffset = $Reader.ReadUInt32()
  202.         $EntryCount = $DataOffset / 320 # entry-size
  203.         Write-Verbose "    DataOffest:  $DataOffset"
  204.         Write-Verbose "    EntryCount:  $EntryCount"
  205.  
  206.         $Stream.Seek(32, [System.IO.SeekOrigin]::Begin) | Out-Null
  207.  
  208.         $Buffer = New-Object System.Text.StringBuilder
  209.         for ($i = 0; $i -lt $EntryCount; ++$i) {
  210.             Write-Progress -Id 1 -ParentId 0 `
  211.                 -Activity 'Reading Bundle' `
  212.                 -Status ($FilePath.Substring($Script:GameRoot.Length + 1)) `
  213.                 -PercentComplete ($i / $EntryCount * 100)
  214.  
  215.             $Stream.Seek(32 + $i * 320, [System.IO.SeekOrigin]::Begin) | Out-Null
  216.  
  217.             $Char = $Reader.ReadChar()
  218.             while ($Char -ne [char]0) {
  219.                 $Buffer.Append($Char) | Out-Null
  220.  
  221.                 $Char = $Reader.ReadChar()
  222.             }
  223.             $ContentList += $Buffer.ToString()
  224.  
  225.             $Buffer.Length = 0
  226.         }
  227.  
  228.         Write-Progress -Id 1 -ParentId 0 `
  229.             -Activity 'Reading Bundle' `
  230.             -Status ($FilePath.Substring($Script:GameRoot.Length + 1)) `
  231.             -Completed
  232.     }
  233.     finally {
  234.         if ($Stream) {
  235.             $Stream.Close()
  236.         }
  237.     }
  238.  
  239.     return $ContentList
  240. }
  241.  
  242.  
  243. function BuildModPriorityList
  244. {
  245.     [CmdletBinding()]
  246.     param()
  247.  
  248.     $Script:ModPriorityList = New-Object -TypeName 'System.Collections.Generic.List[psobject]'
  249.     foreach ($ModDir in (Get-ChildItem -Path "$Script:GameRoot\mods" -Directory -Include 'mod*')) {
  250.         $Script:ModPriorityList.Add($ModDir.Name)
  251.     }
  252.     $Script:ModPriorityList = $Script:ModPriorityList | Sort-Object
  253.  
  254.     if ($PSBoundParameters['Verbose']) {
  255.         Write-Verbose "Found $($Script:ModPriorityList.Length) Mods - Listing in Load Order:"
  256.         for ($i = 0; $i -lt $Script:ModPriorityList.Length; ++$i) {
  257.             Write-Verbose ('  {0,2}:  {1}' -f $i, $Script:ModPriorityList[$i])
  258.         }
  259.     }
  260. }
  261.  
  262.  
  263. function BuildModFileMap
  264. {
  265.     [CmdletBinding()]
  266.     param()
  267.  
  268.     $Script:ModFileMap = @{}
  269.     $Script:ModFileMap | Add-Member -MemberType ScriptMethod -Name AddEntry -Value {
  270.         param(
  271.             [string]$File,
  272.             [int]$ModIndex
  273.         )
  274.         if (-not ($this.ContainsKey($File))) {
  275.             $this[$File] = New-Object -TypeName 'System.Collections.Generic.List[int]'
  276.         }
  277.         $this[$File].Add($ModIndex)
  278.     }
  279.  
  280.     $NormalizedFile = ''
  281.     for ($i = 0; $i -lt $Script:ModPriorityList.Count; ++$i) {
  282.         $ModContentRoot = "$Script:GameRoot\mods\$($Script:ModPriorityList[$i])\content"
  283.  
  284.         Write-Progress -Id 0 -Activity 'Building mod file map' `
  285.             -Status ($Script:ModPriorityList[$i]) `
  286.             -PercentComplete ($i / $Script:ModPriorityList.Count * 100)
  287.         Write-Verbose "Content From:  $($Script:ModPriorityList[$i])"
  288.  
  289.         foreach ($BundleFile in (Get-ChildItem -Path $ModContentRoot -Include '*.bundle' `
  290.                                      -Recurse -File -ErrorAction SilentlyContinue)) {
  291.             foreach ($File in (ExtractContentListFromBundle -FilePath $BundleFile)) {
  292.                 $NormalizedFile = $File.ToLowerInvariant()
  293.  
  294.                 Write-Verbose "  Bundle:  $NormalizedFile"
  295.                 $Script:ModFileMap.AddEntry($NormalizedFile, $i)
  296.             }
  297.         }
  298.        
  299.         $Path = "$ModContentRoot\scripts"
  300.         if (Test-Path $Path -PathType Container -ErrorAction SilentlyContinue) {
  301.             foreach ($File in (Get-ChildItem $Path -Include '*.ws' -Recurse -File)) {
  302.                 $NormalizedFile = $File.FullName.Substring($ModContentRoot.Length + 1).ToLowerInvariant()
  303.  
  304.                 Write-Verbose "  Script:  $NormalizedFile"
  305.                 $Script:ModFileMap.AddEntry($NormalizedFile, $i)
  306.             }
  307.         }
  308.     }
  309.  
  310.     Write-Progress -Id 0 -Activity 'Building mod file map' -Completed
  311. }
  312.  
  313.  
  314. function GetModFileConflicts
  315. {
  316.     [CmdletBinding()]
  317.     param(
  318.         [Parameter(Position = 0, Mandatory = $true)]
  319.         [string]$File
  320.     )
  321.     Write-Verbose "Testing File:  $File"
  322.  
  323.     $ModList = New-Object -TypeName 'System.Collections.Generic.List[string]'
  324.  
  325.     if ($Script:ModFileMap[$File].Count -gt 1) {
  326.         foreach ($index in $Script:ModFileMap[$File]) {
  327.             $ModList.Add($Script:ModPriorityList[$index])
  328.         }
  329.     }
  330.  
  331.     return $ModList
  332. }
  333.  
  334.  
  335. function PrintSingleModConflictReport
  336. {
  337.     [CmdletBinding()]
  338.     param(
  339.         [Parameter(Position = 0, Mandatory = $true)]
  340.         [string]$File,
  341.  
  342.         [Parameter(Position = 0, Mandatory = $true)]
  343.         [psobject[]]$ModList
  344.     )
  345.  
  346.     Write-Host '    [' -ForegroundColor Gray -NoNewline
  347.     Write-Host $File -ForegroundColor Cyan -NoNewline
  348.     Write-Host '] [' -ForegroundColor Gray -NoNewline
  349.     Write-Host $ModList.Length -NoNewline
  350.     Write-Host ']' -ForegroundColor Gray
  351.  
  352.     for ($i = 0; $i -lt $ModList.Length; ++$i) {
  353.         Write-Host '      ' -NoNewline
  354.         if ($i -eq 0) {
  355.             Write-Host $ModList[$i] -ForegroundColor Green
  356.         }
  357.         else {
  358.             Write-Host $ModList[$i]
  359.         }
  360.     }
  361. }
  362.  
  363.  
  364. function PrintModConflictReport
  365. {
  366.     [CmdletBinding()]
  367.     param(
  368.         [Parameter(Position = 0, Mandatory = $true)]
  369.         [hashtable]$ConflictMap
  370.     )
  371.  
  372.     Write-Host '---' -ForegroundColor Gray -NoNewline
  373.     Write-Host ' MOD CONFLICT REPORT /' -NoNewline
  374.     Write-Host '---------------------------------------------------------' `
  375.         -ForegroundColor Gray
  376.  
  377.     Write-Host '  Mod Count:       ' -NoNewline
  378.     Write-Host ($Script:ModPriorityList.Count) -ForegroundColor Yellow
  379.  
  380.     Write-Host '  File Count:      ' -NoNewline
  381.     Write-Host ($Script:ModFileMap.Count) -ForegroundColor Yellow
  382.  
  383.     Write-Host '  Conflict Count:  ' -NoNewline
  384.     if ($ConflictMap.Count -eq 0) {
  385.         Write-Host ($ConflictMap.Count) -ForegroundColor Green
  386.     }
  387.     else {
  388.         Write-Host ($ConflictMap.Count) -ForegroundColor Red
  389.  
  390.         Write-Host '  Conflicts:'
  391.         Write-Host
  392.         foreach ($ModFile in $ConflictMap.Keys) {
  393.             PrintSingleModConflictReport -File $ModFile -ModList $ConflictMap[$ModFile]
  394.             Write-Host
  395.         }
  396.     }
  397.  
  398.     Write-Host '---------------------------------------------------------' `
  399.         -ForegroundColor Gray -NoNewline
  400.     Write-Host '/ MOD CONFLICT REPORT ' -NoNewline
  401.     Write-Host '---' -ForegroundColor Gray
  402. }
  403.  
  404.  
  405. function GetAllModFileConflicts
  406. {
  407.     [CmdletBinding()]
  408.     param()
  409.  
  410.     $ConflictMap = @{}
  411.  
  412.     foreach ($ModFile in $Script:ModFileMap.Keys) {
  413.         $ModList = GetModFileConflicts -File $ModFile
  414.         if ($ModList.Length -gt 0) {
  415.             $ConflictMap[$ModFile] = $ModList
  416.         }
  417.     }
  418.  
  419.     if ($PrintReport) {
  420.         PrintModConflictReport -ConflictMap $ConflictMap
  421.  
  422.         if ($BlockOnConflict -and $ConflictMap.Count -ne 0) {
  423.             Write-Host 'Press <Enter> to continue.'
  424.             [System.Console]::ReadLine() | Out-Null
  425.         }
  426.     }
  427.     else {
  428.         return $ConflictMap
  429.     }
  430. }
  431.  
  432.  
  433. #------------------------------------------------------------------------------
  434.  
  435. Clear-Host
  436. ResolveWitcherInstallDirectory
  437. BuildModPriorityList
  438. BuildModFileMap
  439. GetAllModFileConflicts
Advertisement
Add Comment
Please, Sign In to add comment