CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<#
|
||||
One-off migration: old install layout -> current layout.
|
||||
|
||||
old: <library>\installs\<Name>-<4 hex>\game\ChilloutVR.exe
|
||||
new: <library>\installs\<Name>\ChilloutVR.exe
|
||||
|
||||
Also registers install folders that exist on disk but are missing from config.json
|
||||
(name taken from the folder, branch from the game's appmanifest BetaKey).
|
||||
|
||||
Dry run by default. Nothing is changed unless -Apply is given. Run with pwsh.
|
||||
pwsh -File 2026-10-05-flatten-install-layout.ps1 # show the plan
|
||||
pwsh -File 2026-10-05-flatten-install-layout.ps1 -Apply # do it
|
||||
|
||||
Close CVRManager and every game from the library first. config.json is backed up next to itself.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigDir = (Join-Path $env:APPDATA 'CVRManager'),
|
||||
[switch]$Apply,
|
||||
[switch]$SkipAppCheck # only for testing against a throwaway copy; skips the 'CVRManager is running' check
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-SafeFolderName([string]$name) {
|
||||
$bad = [IO.Path]::GetInvalidFileNameChars()
|
||||
$clean = -join ($name.ToCharArray() | ForEach-Object { if ($bad -contains $_) { ' ' } else { $_ } })
|
||||
while ($clean.Contains(' ')) { $clean = $clean.Replace(' ', ' ') }
|
||||
$clean = $clean.Trim().TrimEnd('.')
|
||||
if ($clean.Length -gt 60) { $clean = $clean.Substring(0, 60).TrimEnd() }
|
||||
if ($clean.Length -eq 0) { 'Install' } else { $clean }
|
||||
}
|
||||
|
||||
$configPath = Join-Path $ConfigDir 'config.json'
|
||||
if (-not (Test-Path $configPath)) { throw "No config at $configPath" }
|
||||
$cfg = Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
$lib = if ($cfg.LibraryRoot) { $cfg.LibraryRoot } else { Join-Path $env:LOCALAPPDATA 'CVRManager' }
|
||||
$installs = Join-Path $lib 'installs'
|
||||
if (-not (Test-Path $installs)) { 'No installs folder; nothing to do.'; return }
|
||||
|
||||
$running = @(if (-not $SkipAppCheck) { Get-Process CVRManager -ErrorAction SilentlyContinue }) + @(Get-Process ChilloutVR -ErrorAction SilentlyContinue | Where-Object { $_.Path -and $_.Path.StartsWith($installs, [StringComparison]::OrdinalIgnoreCase) })
|
||||
if ($Apply -and $running.Count -gt 0) { throw "Close CVRManager and any game started from the library first (running: $(($running | ForEach-Object { $_.ProcessName + ' ' + $_.Id }) -join ', '))." }
|
||||
|
||||
$list = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($i in @($cfg.Installs)) { $list.Add($i) }
|
||||
$newEntries = @()
|
||||
|
||||
# orphan folders: on disk, not in config
|
||||
$known = @($list | ForEach-Object { $_.Folder.ToLowerInvariant() })
|
||||
foreach ($dir in Get-ChildItem $installs -Directory) {
|
||||
if ($known -contains $dir.Name.ToLowerInvariant()) { continue }
|
||||
$hasExe = (Test-Path (Join-Path $dir.FullName 'ChilloutVR.exe')) -or (Test-Path (Join-Path $dir.FullName 'game\ChilloutVR.exe'))
|
||||
if (-not $hasExe) { continue }
|
||||
$m = [regex]::Match($dir.Name, '^(.*)-([0-9a-f]{4})$')
|
||||
$name = if ($m.Success) { $m.Groups[1].Value.Replace('-', ' ').Trim() } else { $dir.Name }
|
||||
$branch = 'public'
|
||||
foreach ($acf in @((Join-Path $dir.FullName 'game\steamapps\appmanifest_661130.acf'), (Join-Path $dir.FullName 'steamapps\appmanifest_661130.acf'))) {
|
||||
if (Test-Path $acf) {
|
||||
$b = [regex]::Match((Get-Content $acf -Raw), '"UserConfig"\s*\{[^}]*?"BetaKey"\s*"([^"]*)"')
|
||||
if ($b.Success -and $b.Groups[1].Value) { $branch = $b.Groups[1].Value; break }
|
||||
}
|
||||
}
|
||||
$defaultHooks = @($cfg.Hooks | Where-Object { $_.DefaultOn } | ForEach-Object { $_.Id })
|
||||
$entry = [pscustomobject]@{
|
||||
Id = ([guid]::NewGuid().ToString('N')).Substring(0, 8); Name = $name; Folder = $dir.Name; Branch = $branch
|
||||
BranchPassword = ''; LaunchArgs = ''; LaunchMode = 'desktop'; Mods = @(); HookIds = $defaultHooks; Deployed = [pscustomobject]@{}
|
||||
}
|
||||
$list.Add($entry); $newEntries += $entry
|
||||
}
|
||||
|
||||
function Test-Taken([string]$candidate, $self) {
|
||||
foreach ($o in $list) { if ($o -ne $self -and $o.Folder -ieq $candidate) { return $true } }
|
||||
if ($candidate -ieq $self.Folder) { return $false }
|
||||
return (Test-Path (Join-Path $installs $candidate))
|
||||
}
|
||||
|
||||
$plans = @()
|
||||
foreach ($i in $list) {
|
||||
$legacy = Join-Path (Join-Path $installs $i.Folder) 'game'
|
||||
$needsFlatten = (Test-Path $legacy) -and ((Test-Path (Join-Path $legacy 'ChilloutVR.exe')) -or -not (Test-Path (Join-Path (Join-Path $installs $i.Folder) 'ChilloutVR.exe')))
|
||||
$base = Get-SafeFolderName $i.Name
|
||||
$candidate = $base; $n = 2
|
||||
while (Test-Taken $candidate $i) { $candidate = "$base ($n)"; $n++ }
|
||||
$needsRename = $candidate -cne $i.Folder
|
||||
if ($needsFlatten -or $needsRename) { $plans += [pscustomobject]@{ Install = $i; From = $i.Folder; To = $candidate; Flatten = $needsFlatten; Rename = $needsRename; Registers = ($newEntries -contains $i) } }
|
||||
}
|
||||
|
||||
"Library: $lib"
|
||||
if ($newEntries.Count -gt 0) { "Install folders on disk that are missing from the config ($($newEntries.Count)):"; $newEntries | ForEach-Object { " + register '$($_.Name)' (folder '$($_.Folder)', branch '$($_.Branch)')" } }
|
||||
if ($plans.Count -eq 0 -and $newEntries.Count -eq 0) { 'Nothing to do: every install already uses the current layout.'; return }
|
||||
"Planned moves:"
|
||||
foreach ($p in $plans) {
|
||||
$what = @(); if ($p.Rename) { $what += "rename folder '$($p.From)' -> '$($p.To)'" }; if ($p.Flatten) { $what += "move the contents of 'game' up one level" }
|
||||
" - $($p.Install.Name): $($what -join '; ')"
|
||||
}
|
||||
if (-not $Apply) { ''; 'Dry run only. Re-run with -Apply to make these changes.'; return }
|
||||
|
||||
$backup = "$configPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
Copy-Item $configPath $backup
|
||||
"Config backed up to $backup"
|
||||
|
||||
function Save-Config([object[]]$installsToWrite) {
|
||||
$cfg.Installs = @($installsToWrite)
|
||||
[IO.File]::WriteAllText($configPath, ($cfg | ConvertTo-Json -Depth 30), (New-Object Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
# progress is written after every install, so a failure part-way never leaves the config out of step with the disk
|
||||
$registered = @($list | Where-Object { $newEntries -notcontains $_ }) # installs that were already in the config stay there
|
||||
$failed = @()
|
||||
foreach ($p in $plans) {
|
||||
$oldDir = Join-Path $installs $p.From
|
||||
$newDir = Join-Path $installs $p.To
|
||||
try {
|
||||
if ($p.Rename) {
|
||||
if ($oldDir -ieq $newDir) { $tmp = "$oldDir.renaming"; [IO.Directory]::Move($oldDir, $tmp); [IO.Directory]::Move($tmp, $newDir) }
|
||||
else { [IO.Directory]::Move($oldDir, $newDir) }
|
||||
$p.Install.Folder = $p.To
|
||||
}
|
||||
$legacy = Join-Path $newDir 'game'
|
||||
if ($p.Flatten -and (Test-Path $legacy)) {
|
||||
foreach ($entry in Get-ChildItem $legacy -Force) {
|
||||
$dest = Join-Path $newDir $entry.Name
|
||||
if (Test-Path $dest) { Write-Warning "Skipped '$($entry.Name)' for $($p.Install.Name): already exists at the destination"; continue }
|
||||
if ($entry.PSIsContainer) { [IO.Directory]::Move($entry.FullName, $dest) } else { [IO.File]::Move($entry.FullName, $dest) }
|
||||
}
|
||||
if (-not (Get-ChildItem $legacy -Force)) { Remove-Item $legacy }
|
||||
}
|
||||
if ($newEntries -contains $p.Install) { $registered += $p.Install }
|
||||
Save-Config (@($list | Where-Object { $newEntries -notcontains $_ -or $registered -contains $_ }))
|
||||
" done: $($p.Install.Name) -> $newDir"
|
||||
}
|
||||
catch [UnauthorizedAccessException], [IO.IOException] {
|
||||
$failed += $p.Install.Name
|
||||
Write-Warning "$($p.Install.Name): $($_.Exception.Message)"
|
||||
Write-Warning "A folder of this install is probably open in Explorer or another program. Close it and run the script again; it continues where it stopped."
|
||||
Save-Config (@($list | Where-Object { $newEntries -notcontains $_ -or $registered -contains $_ }))
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) { "Finished with problems for: $($failed -join ', '). Everything else was migrated and saved." }
|
||||
else { 'Config updated. Start CVRManager again.' }
|
||||
Reference in New Issue
Block a user