#Requires -RunAsAdministrator <# .SYNOPSIS Bootstraps a fresh Windows 11 gaming box: launchers, comms, drivers, sane defaults, and a cheat sheet for the larryfargo.games servers. Idempotent -- safe to re-run. .DESCRIPTION - Installs apps with winget (built into Windows 11, no bootstrap needed) - Detects the GPU vendor and installs the matching driver app - Applies reversible gaming tweaks (Game Mode, high-performance power plan) - Optionally joins the tailnet - Drops a server cheat sheet on the Desktop Every install is fail-soft: a bad package ID is reported at the end, it does not abort the run. .PARAMETER Preset Which app set to install. "core" is launchers + comms + utilities. "full" adds the extra launchers and monitoring tools. .PARAMETER Streaming Also install the streaming kit (OBS, chat client, audio routing) and apply the streaming-specific Windows tweaks. Writes a go-live checklist to the Desktop for the parts a script cannot do. .PARAMETER VTuber Also write the VTuber setup guide. Implies -Streaming. Most VTuber software ships through Steam or direct download rather than winget, so this mostly produces a guide rather than installs. .PARAMETER TailscaleAuthKey Tailscale is installed but deliberately NOT logged in -- the box joins the tailnet only if someone decides it should. Passing a pre-auth key here joins it unattended instead. .PARAMETER ClaudeDirs Directories Claude is allowed to read and write through the MCP filesystem server. Defaults to the OBS config folder, Desktop and Documents. Keep this list short -- see the comment above the Claude section for why. .PARAMETER ObsWebSocket Add a firewall rule for the OBS WebSocket server on 4455, scoped to the Private profile. Only needed if a Stream Deck or phone remote drives OBS. .PARAMETER SkipClaudeFs Install Claude but do not give it filesystem access. .PARAMETER Interactive Walk through every component one at a time with a description of what it does, answering y or n. Prints the equivalent -Components line at the end so the same choices can be replayed without the questions. .PARAMETER Components Comma-separated component keys to run, e.g. "steam,obs,defender". Anything not listed is skipped. Run with -ListComponents to see the keys. .PARAMETER ListComponents Print every component with its description and default, then exit. Changes nothing. .PARAMETER SkipTweaks Install software only. Skips the gaming tweaks, the debloat pass and the Defender hardening -- nothing outside winget gets touched. .PARAMETER DryRun Print what would happen and change nothing. .EXAMPLE .\setup-gaming-box.ps1 .EXAMPLE .\setup-gaming-box.ps1 -Preset full -Streaming -VTuber .EXAMPLE .\setup-gaming-box.ps1 -Streaming -DryRun #> [CmdletBinding()] param( [ValidateSet("core", "full")] [string]$Preset = "core", [switch]$Streaming, [switch]$VTuber, [string]$TailscaleAuthKey, [string[]]$ClaudeDirs, [switch]$SkipClaudeFs, [switch]$ObsWebSocket, [switch]$SkipTweaks, [switch]$Interactive, [string]$Components, [switch]$ListComponents, [switch]$DryRun ) $ErrorActionPreference = "Stop" if ($VTuber) { $Streaming = $true } # ----------------------------------------------------------------------------- # Logging # ----------------------------------------------------------------------------- # Every run is transcribed, start to finish, before anything else happens. When # this script dies on someone else's machine the transcript is the only thing # that makes it fixable, so it is not optional and not behind a flag. $LogDir = "$env:USERPROFILE\Diagnostics" try { New-Item -Path $LogDir -ItemType Directory -Force -ErrorAction Stop | Out-Null } catch { $LogDir = $env:TEMP } $LogPath = Join-Path $LogDir "setup-$(Get-Date -Format 'yyyy-MM-dd_HHmmss').log" $script:Transcribing = $false try { Start-Transcript -Path $LogPath -Force -ErrorAction Stop | Out-Null $script:Transcribing = $true } catch { Write-Warning "Could not start a transcript: $($_.Exception.Message)" } # collect-diagnostics.ps1 prunes directories only, so these would otherwise # accumulate one per run forever. Get-ChildItem $LogDir -Filter "setup-*.log" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -Skip 10 | Remove-Item -Force -ErrorAction SilentlyContinue function Stop-Log { if ($script:Transcribing) { try { Stop-Transcript | Out-Null } catch {} $script:Transcribing = $false } } # $ErrorActionPreference is Stop, so the first unhandled error ends the run. # Without this trap that looks like the script silently doing nothing. Print # what failed, where, and where the log is. trap { Write-Host "" Write-Host " ============================================================" -ForegroundColor Red Write-Host " SCRIPT STOPPED" -ForegroundColor Red Write-Host " $($_.Exception.Message)" -ForegroundColor Red if ($_.InvocationInfo) { Write-Host " line $($_.InvocationInfo.ScriptLineNumber): $($_.InvocationInfo.Line.Trim())" -ForegroundColor Red } Write-Host " ============================================================" -ForegroundColor Red Write-Host "" Write-Host " Full log: $LogPath" Write-Host " Send that file to whoever set this up, or run:" Write-Host " .\send-logs.ps1" Write-Host "" Stop-Log break } # ----------------------------------------------------------------------------- # Component catalog # ----------------------------------------------------------------------------- # Every optional piece of this script, with a plain description. Used two ways: # -Interactive ask about each one in turn # -Components a,b run exactly these # With neither, DefaultWhen decides, which is the original flag behaviour -- so # existing command lines keep working unchanged. $Catalog = @( # --- Software --- @{ Key="steam"; Group="Software"; Name="Steam"; Default=$true; Desc="The game launcher. Almost certainly yes." } @{ Key="discord"; Group="Software"; Name="Discord"; Default=$true; Desc="Voice chat, and how the game servers get started and stopped." } @{ Key="chrome"; Group="Software"; Name="Google Chrome"; Default=$true; Desc="Browser. Setting it as default is a manual click afterwards." } @{ Key="firefox"; Group="Software"; Name="Firefox"; Default=$false; Desc="A second browser. Only if you want one." } @{ Key="onepassword"; Group="Software"; Name="1Password"; Default=$true; Desc="Password manager. Needs a paid subscription or a seat on someone's Families plan, or it stops working after the trial." } @{ Key="claude"; Group="Software"; Name="Claude"; Default=$true; Desc="AI assistant app. Free tier is real. This is how he troubleshoots without calling you." } @{ Key="tailscale"; Group="Software"; Name="Tailscale"; Default=$true; Desc="Private network client. Installed but NOT logged in unless you pass an auth key." } @{ Key="utils"; Group="Software"; Name="Game runtimes"; Default=$true; Desc="Visual C++ redistributables and DirectX. Games genuinely need these. Say yes." } @{ Key="sevenzip"; Group="Software"; Name="7-Zip"; Default=$false; Desc="Windows 11 already opens zip, 7z and rar on its own, so this is optional. Worth it only for password-protected or split archives, which mostly means game mods." } @{ Key="monitoring"; Group="Software"; Name="HWiNFO and CPU-Z"; Default=$false; Desc="Hardware monitoring. Useful when diagnosing overheating, clutter otherwise." } @{ Key="vlc"; Group="Software"; Name="VLC"; Default=$false; Desc="Plays any video file. Nice to have." } # --- Streaming --- @{ Key="obs"; Group="Streaming"; Name="OBS Studio"; Default=$true; Desc="The streaming and recording software itself. Required for streaming." } @{ Key="chatterino"; Group="Streaming"; Name="Chatterino"; Default=$true; Desc="Lightweight Twitch chat window. Much better than reading chat in a browser tab." } @{ Key="voicemeeter"; Group="Streaming"; Name="VoiceMeeter Banana"; Default=$false; Desc="Advanced audio routing, for splitting game/mic/Discord onto separate tracks. If you do not know you need it, you do not." } @{ Key="sharex"; Group="Streaming"; Name="ShareX"; Default=$true; Desc="Screenshots and short clips. Better than the Windows one." } @{ Key="elgato"; Group="Streaming"; Name="Elgato Stream Deck and Wave Link"; Default=$false; Desc="Only if you own Elgato hardware. Pointless otherwise." } @{ Key="gpuapp"; Group="Streaming"; Name="GPU driver app"; Default=$true; Desc="NVIDIA App or AMD Adrenalin. Not on winget, so this just prints the download link. Do it -- drivers matter more than anything else here." } @{ Key="resolve"; Group="Streaming"; Name="DaVinci Resolve"; Default=$false; Desc="Free video editor for cutting VODs. Large download, and also not on winget." } # --- Windows tweaks --- @{ Key="gamemode"; Group="Tweaks"; Name="Game Mode"; Default=$true; Desc="Tells Windows to prioritise the running game. Reversible in Settings." } @{ Key="powerplan"; Group="Tweaks"; Name="High performance power plan"; Default=$true; Desc="Stops the CPU parking cores mid-game. On a desktop there is no downside; on a laptop it costs battery." } @{ Key="fileext"; Group="Tweaks"; Name="Show file extensions"; Default=$true; Desc="So you can tell mod.zip from mod.zip.exe. This is a security thing as much as a convenience." } @{ Key="ducking"; Group="Tweaks"; Name="Disable audio ducking"; Default=$true; Desc="Stops Windows quietening the game whenever it thinks a call started. Very noticeable on stream." } # --- Debloat --- @{ Key="gamedvr"; Group="Debloat"; Name="Turn off Game DVR"; Default=$true; Desc="Windows records gameplay in the background by default. Costs real frames, and OBS replay buffer does it better." } @{ Key="appremoval"; Group="Debloat"; Name="Remove preinstalled apps"; Default=$true; Desc="Candy Crush, Solitaire, Clipchamp, Teams, Bing News and similar. All reinstallable from the Store." } @{ Key="taskbar"; Group="Debloat"; Name="Tidy the taskbar"; Default=$true; Desc="Hides Widgets, Task View and Copilot buttons. Purely cosmetic, trivially undone." } @{ Key="faststartup"; Group="Debloat"; Name="Disable Fast Startup"; Default=$true; Desc="Makes shutdown a real shutdown. Helps driver installs and troubleshooting, costs a few seconds of boot." } @{ Key="telemetry"; Group="Debloat"; Name="Telemetry to Required only"; Default=$true; Desc="One policy setting. Does nothing on Windows Home. Goes no further, because disabling the services breaks Store and Defender updates." } @{ Key="onedrive"; Group="Debloat"; Name="Remove OneDrive"; Default=$false; Desc="Unlinks it before it can redirect Desktop and Documents. Skip this if he actually uses OneDrive -- it is refused automatically if those folders are already redirected." } # --- Security --- @{ Key="defender"; Group="Security"; Name="Defender hardening"; Default=$true; Desc="Turns on PUA protection, network protection and cloud protection. No third-party antivirus is installed; Defender is the right answer." } @{ Key="firewall"; Group="Security"; Name="Firewall check"; Default=$true; Desc="Verifies the firewall is on and blocking inbound, and enables dropped-packet logging. Never deletes existing rules." } @{ Key="claudefs"; Group="Security"; Name="Claude file access"; Default=$true; Desc="Lets Claude read five specific folders so it can diagnose problems. Never AppData wholesale -- that holds browser sessions and password manager data." } # --- Handover --- @{ Key="helppage"; Group="Handover"; Name="START HERE page"; Default=$true; Desc="The plain-English manual on his Desktop. The one thing he needs to know exists." } @{ Key="diagtools"; Group="Handover"; Name="Diagnostic shortcuts"; Default=$true; Desc="Collect Diagnostics and Monitor Connection on the Desktop, so he never opens a terminal." } @{ Key="reference"; Group="Handover"; Name="Reference documents"; Default=$true; Desc="Server list, streaming checklist and the VTuber commissioning guide, filed in Documents." } ) # Fall back to the original flag behaviour when nothing is explicitly selected. $DefaultWhen = @{ firefox = { $Preset -eq "full" } monitoring = { $Preset -eq "full" } vlc = { $Preset -eq "full" } obs = { $Streaming } chatterino = { $Streaming } voicemeeter = { $Streaming } sharex = { $Streaming } elgato = { $Streaming -and $Preset -eq "full" } resolve = { $Streaming -and $Preset -eq "full" } gamemode = { -not $SkipTweaks } powerplan = { -not $SkipTweaks } fileext = { -not $SkipTweaks } ducking = { -not $SkipTweaks -and $Streaming } gamedvr = { -not $SkipTweaks } appremoval = { -not $SkipTweaks } taskbar = { -not $SkipTweaks } faststartup = { -not $SkipTweaks } telemetry = { -not $SkipTweaks } onedrive = { -not $SkipTweaks } defender = { -not $SkipTweaks } firewall = { -not $SkipTweaks } claudefs = { -not $SkipClaudeFs } reference = { $true } } $script:Sel = @{} $script:SelActive = $false function Want($key) { if ($script:SelActive) { return [bool]$script:Sel[$key] } if ($DefaultWhen.ContainsKey($key)) { return [bool](& $DefaultWhen[$key]) } return $true } function Invoke-Picker { $script:SelActive = $true $all = $null # $true = yes to everything left, $false = no to everything left Write-Host "" Write-Host " Pick what you want. Enter accepts the suggestion in brackets." -ForegroundColor Cyan Write-Host " y = yes n = no a = yes to all remaining s = skip all remaining" -ForegroundColor DarkGray $lastGroup = "" foreach ($c in $Catalog) { if ($c.Group -ne $lastGroup) { Write-Host "" Write-Host " --- $($c.Group) ---" -ForegroundColor Cyan $lastGroup = $c.Group } if ($null -ne $all) { $script:Sel[$c.Key] = $all; continue } $hint = if ($c.Default) { "Y/n" } else { "y/N" } Write-Host "" Write-Host " $($c.Name)" -ForegroundColor White Write-Host " $($c.Desc)" -ForegroundColor DarkGray while ($true) { $ans = (Read-Host " yes? [$hint]").Trim().ToLower() if ($ans -eq "") { $script:Sel[$c.Key] = $c.Default; break } if ($ans -eq "y") { $script:Sel[$c.Key] = $true; break } if ($ans -eq "n") { $script:Sel[$c.Key] = $false; break } if ($ans -eq "a") { $all = $true; $script:Sel[$c.Key] = $true; break } if ($ans -eq "s") { $all = $false; $script:Sel[$c.Key] = $false; break } Write-Host " y, n, a or s." -ForegroundColor Yellow } } $chosen = @($Catalog | Where-Object { $script:Sel[$_.Key] } | ForEach-Object { $_.Key }) Write-Host "" Write-Host " Chose $($chosen.Count) of $($Catalog.Count)." -ForegroundColor Cyan Write-Host " To replay this exact set without the questions:" -ForegroundColor DarkGray Write-Host " .\setup-gaming-box.ps1 -Components `"$($chosen -join ',')`"" -ForegroundColor DarkGray Write-Host "" $null = Read-Host " Enter to continue, Ctrl-C to bail" } if ($ListComponents) { $lastGroup = "" foreach ($c in $Catalog) { if ($c.Group -ne $lastGroup) { Write-Host "" Write-Host " --- $($c.Group) ---" -ForegroundColor Cyan $lastGroup = $c.Group } $def = if ($c.Default) { "on " } else { "off" } Write-Host "" Write-Host (" [{0}] {1,-16} {2}" -f $def, $c.Key, $c.Name) -ForegroundColor White Write-Host " $($c.Desc)" -ForegroundColor DarkGray } Write-Host "" Write-Host " Pick interactively: .\setup-gaming-box.ps1 -Interactive" Write-Host " Or name them: .\setup-gaming-box.ps1 -Components `"steam,obs,defender`"" Write-Host "" Stop-Log return } if ($Components) { $script:SelActive = $true $keys = $Components -split "," | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ } $valid = $Catalog | ForEach-Object { $_.Key } foreach ($k in $keys) { if ($valid -notcontains $k) { Write-Warning "Unknown component '$k' -- ignoring." } else { $script:Sel[$k] = $true } } } elseif ($Interactive) { Invoke-Picker } # ----------------------------------------------------------------------------- # What we install # ----------------------------------------------------------------------------- # Tier: core = everybody needs it # full = -Preset full # stream = -Streaming # stream-extra = -Streaming AND -Preset full # Ids are winget package ids. Verify one with: winget show --id $Apps = @( # --- Launchers --- # Steam and nothing else, by decision. No Epic/GOG/EA/Ubisoft, and no # Minecraft launcher -- he does not play it. The Minecraft servers still # exist in gaming/servers and are still on the cheat sheet; if he ever # wants in, Prism is a two-minute install then. @{ Id = "Valve.Steam"; Name = "Steam"; Comp = "steam" } # --- Comms (the Discord bot runs the servers, so this one is not optional) --- @{ Id = "Discord.Discord"; Name = "Discord"; Comp = "discord" } # --- Runtimes --- @{ Id = "Microsoft.VCRedist.2015+.x64"; Name = "VC++ x64"; Comp = "utils" } @{ Id = "Microsoft.VCRedist.2015+.x86"; Name = "VC++ x86"; Comp = "utils" } @{ Id = "Microsoft.DirectX"; Name = "DirectX runtime"; Comp = "utils" } # --- Utilities --- @{ Id = "7zip.7zip"; Name = "7-Zip"; Comp = "sevenzip" } @{ Id = "Google.Chrome"; Name = "Chrome"; Comp = "chrome" } @{ Id = "Mozilla.Firefox"; Name = "Firefox"; Comp = "firefox" } @{ Id = "AgileBits.1Password"; Name = "1Password"; Comp = "onepassword" } @{ Id = "Anthropic.Claude"; Name = "Claude"; Comp = "claude" } @{ Id = "OpenJS.NodeJS.LTS"; Name = "Node.js LTS"; Comp = "claudefs" } # for the MCP server @{ Id = "Tailscale.Tailscale"; Name = "Tailscale"; Comp = "tailscale" } @{ Id = "REALiX.HWiNFO"; Name = "HWiNFO"; Comp = "monitoring" } @{ Id = "CPUID.CPU-Z"; Name = "CPU-Z"; Comp = "monitoring" } @{ Id = "VideoLAN.VLC"; Name = "VLC"; Comp = "vlc" } # --- Streaming --- @{ Id = "OBSProject.OBSStudio"; Name = "OBS Studio"; Comp = "obs" } @{ Id = "ChatterinoTeam.Chatterino"; Name = "Chatterino (chat)"; Comp = "chatterino" } @{ Id = "VB-Audio.Voicemeeter.Banana"; Name = "VoiceMeeter Banana"; Comp = "voicemeeter" } @{ Id = "ShareX.ShareX"; Name = "ShareX"; Comp = "sharex" } @{ Id = "Elgato.StreamDeck"; Name = "Elgato Stream Deck"; Comp = "elgato" } @{ Id = "Elgato.WaveLink"; Name = "Elgato Wave Link"; Comp = "elgato" } ) # ----------------------------------------------------------------------------- # The servers (mirrors gaming/servers/main.tf + gaming/dns/main.tf) # ----------------------------------------------------------------------------- $Servers = @( @{ Game = "Enshrouded"; Address = "enshrouded.larryfargo.games"; Port = "15636 (UDP)"; Note = "in-game server browser: add by IP:port" } @{ Game = "Minecraft (Java)"; Address = "minecraft-java.larryfargo.games"; Port = "25565"; Note = "Add Server in Prism / vanilla launcher" } @{ Game = "Minecraft Bedrock"; Address = "minecraft-bedrock.larryfargo.games"; Port = "19132 (UDP)"; Note = "Servers tab -> Add Server" } @{ Game = "RimWorld"; Address = "rimworld.larryfargo.games"; Port = "25555"; Note = "" } ) # ----------------------------------------------------------------------------- # Helpers # ----------------------------------------------------------------------------- $script:Failures = @() $script:Installed = @() $script:Skipped = @() $script:Manual = @() # Verified against the live winget catalogue on 2026-09-18: GPU vendor apps and # DaVinci Resolve simply are not published there. Pretending otherwise produced # a run that reported success and installed no driver software at all. function Add-Manual($name, $url) { $script:Manual += [PSCustomObject]@{ Name = $name; Url = $url } Write-Host " -- $name is not on winget; queued as a manual download" } function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan } # Wrapper for every registry write in this script. Returns $true on success and # warns without throwing on failure, so one blocked value cannot abort the run. function Set-Reg($path, $name, $value, $label) { try { if (-not (Test-Path $path)) { New-Item -Path $path -Force -ErrorAction Stop | Out-Null } Set-ItemProperty -Path $path -Name $name -Value $value -Type DWord -ErrorAction Stop return $true } catch { $what = if ($label) { $label } else { "$name" } Write-Bad "could not set $what ($($_.Exception.Message.Trim()))" $script:Failures += "registry: $what" return $false } } function Write-Ok($msg) { Write-Host " OK $msg" -ForegroundColor Green } function Write-Skip($msg) { Write-Host " -- $msg" -ForegroundColor DarkGray } function Write-Bad($msg) { Write-Host " !! $msg" -ForegroundColor Yellow } function Test-WingetPackage($id) { $out = winget list --id $id --exact --accept-source-agreements 2>$null | Out-String return $out -match [regex]::Escape($id) } function Install-App($app) { if (Test-WingetPackage $app.Id) { Write-Skip "$($app.Name) already installed" $script:Skipped += $app.Name return } if ($DryRun) { Write-Host " DRY would install $($app.Name) ($($app.Id))" return } Write-Host " ... installing $($app.Name)" winget install --id $app.Id --exact --silent ` --accept-package-agreements --accept-source-agreements ` --disable-interactivity 2>&1 | Out-Null if ($LASTEXITCODE -eq 0 -or (Test-WingetPackage $app.Id)) { Write-Ok $app.Name $script:Installed += $app.Name } else { Write-Bad "$($app.Name) failed (winget exit $LASTEXITCODE, id '$($app.Id)')" $script:Failures += "$($app.Name) [$($app.Id)]" } } # ----------------------------------------------------------------------------- # Preflight # ----------------------------------------------------------------------------- Write-Step "Preflight" $os = Get-CimInstance Win32_OperatingSystem Write-Host " $($os.Caption) build $($os.BuildNumber)" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Error @" winget not found. On a fresh Windows 11 box it ships with 'App Installer'. Fix: open the Microsoft Store, search 'App Installer', install/update it, then re-run this script from a new admin PowerShell. "@ exit 1 } Write-Ok "winget $(winget --version)" if ($DryRun) { Write-Host "`n DRY RUN -- nothing will be changed." -ForegroundColor Magenta } # ----------------------------------------------------------------------------- # GPU driver app # ----------------------------------------------------------------------------- Write-Step "GPU" $gpus = @(Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) foreach ($g in $gpus) { Write-Host " detected: $g" } # NVIDIA and AMD do not publish their driver software to winget. Only Intel # does. So for the two that matter this queues a download instead of silently # doing nothing. if (-not (Want "gpuapp")) { Write-Skip "GPU driver app skipped by choice" } elseif ($gpus -match "NVIDIA") { Add-Manual "NVIDIA App (drivers + control panel)" "https://www.nvidia.com/en-us/software/nvidia-app/" if ($Streaming) { Add-Manual "NVIDIA Broadcast (mic noise removal)" "https://www.nvidia.com/en-us/geforce/broadcasting/broadcast-app/" } } elseif ($gpus -match "AMD|Radeon") { Add-Manual "AMD Software: Adrenalin Edition" "https://www.amd.com/en/support/download/drivers.html" } elseif ($gpus -match "Intel") { Install-App @{ Id = "Intel.IntelDriverAndSupportAssistant"; Name = "Intel DSA"; Tier = "core" } } else { Write-Bad "No known GPU vendor matched -- install the driver app by hand." } # ----------------------------------------------------------------------------- # Apps # ----------------------------------------------------------------------------- Write-Step "Apps" $wanted = $Apps | Where-Object { Want $_.Comp } foreach ($app in $wanted) { Install-App $app } if ((Want "resolve")) { Add-Manual "DaVinci Resolve (free video editor)" "https://www.blackmagicdesign.com/products/davinciresolve" } # Tailscale is installed with the core apps above. By default we stop there: # it sits on the box unauthenticated until someone chooses to log it in. if ($TailscaleAuthKey) { Write-Step "Tailscale" if (-not $DryRun) { $ts = "$env:ProgramFiles\Tailscale\tailscale.exe" if (Test-Path $ts) { Write-Host " ... joining tailnet" & $ts up --authkey $TailscaleAuthKey --accept-routes 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Ok "joined tailnet" } else { Write-Bad "tailscale up failed (exit $LASTEXITCODE) -- run it by hand" } } else { Write-Bad "tailscale.exe not found at $ts -- log in from the tray icon" } } } # ----------------------------------------------------------------------------- # Windows tweaks (all reversible) # ----------------------------------------------------------------------------- if ((Want "gamemode") -or (Want "powerplan") -or (Want "fileext") -or (Want "ducking")) { Write-Step "Windows tweaks" if ($DryRun) { Write-Host " DRY would: Game Mode on, High performance power plan, show file extensions" } else { # Game Mode on $gameBar = "HKCU:\Software\Microsoft\GameBar" New-Item -Path $gameBar -Force | Out-Null $null = Set-Reg $gameBar "AllowAutoGameMode" 1 "Game Mode (auto)" $null = Set-Reg $gameBar "AutoGameModeEnabled" 1 "Game Mode" Write-Ok "Game Mode enabled" # High performance power plan (a desktop should never be parking cores mid-raid) $high = "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c" powercfg /setactive $high 2>$null if ($LASTEXITCODE -eq 0) { Write-Ok "High performance power plan active" } else { Write-Bad "could not set power plan -- set it in Settings > Power" } # Show file extensions (so he can tell mod.zip from mod.zip.exe) $explorer = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" $null = Set-Reg $explorer "HideFileExt" 0 "show file extensions" Write-Ok "File extensions visible" if ($Streaming) { # Windows ducks every other sound when it thinks a call started. # On stream that reads as the game randomly going quiet. 3 = do nothing. $audio = "HKCU:\Software\Microsoft\Multimedia\Audio" New-Item -Path $audio -Force | Out-Null $null = Set-Reg $audio "UserDuckingPreference" 3 "audio ducking" Write-Ok "Audio ducking disabled (Communications = Do nothing)" } } } # ----------------------------------------------------------------------------- # Firewall # ----------------------------------------------------------------------------- # A gaming box needs far fewer inbound rules than people assume: games dial out # to servers, and the game servers in gaming/servers are in AWS, not here. The # Windows default -- block inbound, allow outbound -- is already correct. # # So this verifies rather than loosens, and turns on dropped-packet logging so # collect-diagnostics.ps1 has something to show when "it will not connect". if ((Want "firewall")) { Write-Step "Firewall" if ($DryRun) { Write-Host " DRY would verify profiles and enable dropped-packet logging" } else { foreach ($prof in @("Domain", "Private", "Public")) { $fp = Get-NetFirewallProfile -Name $prof -ErrorAction SilentlyContinue if (-not $fp) { continue } if (-not $fp.Enabled) { Set-NetFirewallProfile -Name $prof -Enabled True Write-Bad "$prof profile was OFF -- turned it back on" } if ($fp.DefaultInboundAction -eq "Allow") { Set-NetFirewallProfile -Name $prof -DefaultInboundAction Block Write-Bad "$prof allowed inbound by default -- set to Block" } Set-NetFirewallProfile -Name $prof ` -LogBlocked True -LogMaxSizeKilobytes 4096 ` -LogFileName "%systemroot%\system32\LogFiles\Firewall\pfirewall.log" } Write-Ok "All profiles on, inbound blocked by default, dropped packets logged" if ($ObsWebSocket) { $ruleName = "OBS WebSocket (LAN only)" if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) { Write-Skip "$ruleName already present" } else { New-NetFirewallRule -DisplayName $ruleName ` -Direction Inbound -Action Allow -Protocol TCP -LocalPort 4455 ` -Profile Private ` -Description "Stream Deck / phone remote control of OBS" | Out-Null Write-Ok "$ruleName added (Private profile only)" } } } } # ----------------------------------------------------------------------------- # Debloat # ----------------------------------------------------------------------------- # Everything here is reversible: the apps reinstall from the Store, the registry # values all have a Settings-app equivalent. No services are disabled, and no # third-party debloat script is involved -- those rot across Windows versions and # leave nobody able to explain what broke three months later. if ((Want "appremoval") -or (Want "gamedvr") -or (Want "taskbar") -or (Want "faststartup") -or (Want "telemetry") -or (Want "onedrive")) { Write-Step "Debloat" # Curated on purpose: a wildcard sweep is how people delete winget. $BloatApps = @( "king.com.CandyCrush" "Microsoft.BingNews" "Microsoft.BingWeather" "Microsoft.MicrosoftSolitaireCollection" "Clipchamp.Clipchamp" "Microsoft.Todos" "Microsoft.PowerAutomateDesktop" "Microsoft.People" "Microsoft.WindowsFeedbackHub" "Microsoft.GetHelp" "Microsoft.Getstarted" "Microsoft.MicrosoftOfficeHub" "Microsoft.SkypeApp" "Microsoft.MixedReality.Portal" "MicrosoftTeams" "MSTeams" ) # Never removed, whatever the list above says. DesktopAppInstaller IS winget: # removing it halfway through this script would be a memorable own goal. $ProtectedApps = @( "Microsoft.DesktopAppInstaller" "Microsoft.WindowsStore" "Microsoft.GamingApp" # Game Pass "Microsoft.XboxGamingOverlay" # some games overlay through it "Microsoft.XboxIdentityProvider" "Microsoft.SecHealthUI" # Defender UI "Microsoft.WindowsTerminal" "Microsoft.VCLibs" "Microsoft.UI.Xaml" "Microsoft.NET.Native" ) $removed = 0 foreach ($pattern in $BloatApps) { $pkgs = Get-AppxPackage -Name "$pattern*" -ErrorAction SilentlyContinue foreach ($pkg in $pkgs) { $guard = $ProtectedApps | Where-Object { $pkg.Name -like "$_*" } if ($guard) { Write-Bad "refusing to remove protected package $($pkg.Name)" continue } if ($DryRun) { Write-Host " DRY would remove $($pkg.Name)" continue } try { Remove-AppxPackage -Package $pkg.PackageFullName -ErrorAction Stop $removed++ } catch { Write-Bad "could not remove $($pkg.Name)" } } } if (-not $DryRun) { Write-Ok "$removed preinstalled app(s) removed" } if ($DryRun) { Write-Host " DRY would: Game DVR off, widgets off, taskbar tidy, Fast Startup off, telemetry to Required" } else { # --- Game DVR: background gameplay recording. Costs frames; OBS does it better. --- $gameStore = "HKCU:\System\GameConfigStore" New-Item -Path $gameStore -Force | Out-Null $null = Set-Reg $gameStore "GameDVR_Enabled" 0 "Game DVR" $gameDvrPol = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" New-Item -Path $gameDvrPol -Force | Out-Null $null = Set-Reg $gameDvrPol "AllowGameDVR" 0 "Game DVR policy" Write-Ok "Game DVR background recording disabled" # --- Widgets + taskbar tidy --- $adv = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" # Windows 11 protects some of these depending on build and policy, and # a blocked one throws rather than failing quietly. Count what took. # TaskbarDa is protected on Windows 11 build 26200: the key's ACL grants # the user FullControl and the three values beside it write fine, but this # one throws "unauthorized operation". Verified by probing them # individually on 2026-09-18. Widgets is a Settings-only toggle now. $tb = 0 if (Set-Reg $adv "TaskbarDa" 0 "widgets button") { $tb++ } else { Write-Host " Widgets is Settings-only on this build:" -ForegroundColor DarkGray Write-Host " Settings > Personalization > Taskbar > Widgets, turn it off." -ForegroundColor DarkGray Add-Manual "Turn off Widgets by hand" "Settings > Personalization > Taskbar" } if (Set-Reg $adv "TaskbarMn" 0 "chat button") { $tb++ } if (Set-Reg $adv "ShowTaskViewButton" 0 "task view button") { $tb++ } if (Set-Reg $adv "ShowCopilotButton" 0 "Copilot button") { $tb++ } Write-Host " ($tb of 4 taskbar tweaks applied)" -ForegroundColor DarkGray $dsh = "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" New-Item -Path $dsh -Force | Out-Null $null = Set-Reg $dsh "AllowNewsAndInterests" 0 "news and interests" Write-Ok "Taskbar tweaks applied where Windows allowed it" # --- Fast Startup off (hibernate itself left alone) --- $power = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" $null = Set-Reg $power "HiberbootEnabled" 0 "Fast Startup" Write-Ok "Fast Startup disabled (shutdown is now a real shutdown)" # --- Telemetry to Required only. Deliberately stops here: disabling the # services breaks Store and Defender updates. The policy key is # ignored on Windows Home, so this may be a no-op there. --- $dc = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" New-Item -Path $dc -Force | Out-Null $null = Set-Reg $dc "AllowTelemetry" 1 "telemetry level" Write-Ok "Telemetry set to Required only (no-op on Home edition)" } # --- OneDrive --- # Guarded: if OneDrive has already swallowed Desktop or Documents, # uninstalling strands those files. On a fresh box it has not, so we unlink # before it ever gets the chance. $desktopPath = [Environment]::GetFolderPath("Desktop") $docsPath = [Environment]::GetFolderPath("MyDocuments") if ($desktopPath -like "*OneDrive*" -or $docsPath -like "*OneDrive*") { Write-Bad "OneDrive already redirects Desktop/Documents -- NOT touching it." Write-Bad " Move those folders back by hand first, then re-run." $script:Failures += "OneDrive unlink (skipped: folders already redirected)" } elseif ($DryRun) { Write-Host " DRY would uninstall OneDrive and block folder sync" } else { Get-Process OneDrive -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue $setup = @( "$env:LOCALAPPDATA\Microsoft\OneDrive\OneDrive.exe" "$env:SystemRoot\SysWOW64\OneDriveSetup.exe" "$env:SystemRoot\System32\OneDriveSetup.exe" ) | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($setup) { Start-Process -FilePath $setup -ArgumentList "/uninstall" -Wait -ErrorAction SilentlyContinue Write-Ok "OneDrive uninstalled" } else { Write-Skip "OneDrive installer not found -- probably already gone" } $od = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive" New-Item -Path $od -Force | Out-Null $null = Set-Reg $od "DisableFileSyncNGSC" 1 "OneDrive sync policy" Write-Ok "OneDrive folder sync blocked" } } # ----------------------------------------------------------------------------- # Defender hardening # ----------------------------------------------------------------------------- # No third-party AV. Defender is the right answer; it just ships with a couple # of genuinely useful things switched off. if ((Want "defender")) { Write-Step "Defender" if ($DryRun) { Write-Host " DRY would: PUA protection on, network protection on, cloud protection on" } else { try { # Blocks bundleware and "free game key generator" style junk that is # technically not malware, which is most of what actually gets people. Set-MpPreference -PUAProtection Enabled Write-Ok "PUA (bundleware) protection enabled" # Blocks known-bad domains at the network layer -- phishing pages, # infostealer C2. This is the one that matters for a streamer. Set-MpPreference -EnableNetworkProtection Enabled Write-Ok "Network protection enabled" Set-MpPreference -MAPSReporting Advanced Set-MpPreference -SubmitSamplesConsent SendSafeSamples Write-Ok "Cloud-delivered protection enabled" } catch { Write-Bad "Defender settings failed: $($_.Exception.Message)" $script:Failures += "Defender hardening" } } # Deliberately NOT set: # Controlled Folder Access -- blocks games writing saves to Documents. # Steam library scan exclusions -- a common perf tip, but mods and # workshop content are exactly what you want scanned, and the real-time # scan cost on an NVMe box is noise. } # ----------------------------------------------------------------------------- # Server cheat sheet # ----------------------------------------------------------------------------- Write-Step "Server cheat sheet" $sheetLines = @() $sheetLines += "larryfargo.games -- server list" $sheetLines += "generated $(Get-Date -Format 'yyyy-MM-dd') by setup-gaming-box.ps1" $sheetLines += "" $sheetLines += "Servers are started and stopped from Discord -- ask the bot, not the host." $sheetLines += "If a connection times out, the box is probably just off. Start it in Discord," $sheetLines += "give it a minute, then connect." $sheetLines += "" foreach ($s in $Servers) { $sheetLines += "$($s.Game)" $sheetLines += " $($s.Address) port $($s.Port)" if ($s.Note) { $sheetLines += " $($s.Note)" } $sheetLines += "" } $refDir = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "Rig Reference" if (-not $DryRun) { New-Item -Path $refDir -ItemType Directory -Force | Out-Null } $sheetPath = Join-Path $refDir "servers.txt" if ($DryRun) { Write-Host " DRY would write $sheetPath" } else { $sheetLines -join "`r`n" | Set-Content -Path $sheetPath -Encoding UTF8 Write-Ok $sheetPath } # ----------------------------------------------------------------------------- # Diagnostics collector # ----------------------------------------------------------------------------- # Event logs and firewall rules are not files, so the MCP filesystem server # cannot reach them -- .evtx is binary. This installs a collector that dumps # them to text inside a folder Claude IS scoped to, plus a Desktop shortcut so # he can run it without opening a terminal. It self-elevates. $diagRoot = "$env:USERPROFILE\Diagnostics" $diagDst = Join-Path $diagRoot "collect-diagnostics.ps1" $helpers = @( @{ File = "collect-diagnostics.ps1"; Shortcut = "Collect Diagnostics"; Icon = 109 } @{ File = "monitor-connection.ps1"; Shortcut = "Monitor Connection"; Icon = 168 } @{ File = "send-logs.ps1"; Shortcut = $null; Icon = 0 } ) Write-Step "Diagnostics tools" if (-not (Want "diagtools")) { Write-Skip "skipped by choice" } $missing = @($helpers | Where-Object { -not (Test-Path (Join-Path $PSScriptRoot $_.File)) }) if (-not (Want "diagtools")) { # nothing to do } elseif ($missing.Count -gt 0) { foreach ($m in $missing) { Write-Bad "$($m.File) not found next to this script" } $script:Failures += "diagnostics tools (files missing)" } elseif ($DryRun) { Write-Host " DRY would install $diagRoot\*.ps1 and two Desktop shortcuts" } else { New-Item -Path $diagRoot -ItemType Directory -Force | Out-Null foreach ($h in $helpers) { Copy-Item (Join-Path $PSScriptRoot $h.File) (Join-Path $diagRoot $h.File) -Force Write-Ok (Join-Path $diagRoot $h.File) } try { $shell = New-Object -ComObject WScript.Shell foreach ($h in $helpers) { if (-not $h.Shortcut) { continue } # send-logs is invoked from the trap, not the Desktop $lnk = Join-Path ([Environment]::GetFolderPath("Desktop")) "$($h.Shortcut).lnk" $sc = $shell.CreateShortcut($lnk) $sc.TargetPath = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" # -NoExit on the monitor so the summary is still on screen after Ctrl-C. $noExit = if ($h.File -eq "monitor-connection.ps1") { "-NoExit " } else { "" } $sc.Arguments = "$noExit-NoProfile -ExecutionPolicy Bypass -File `"$(Join-Path $diagRoot $h.File)`"" $sc.WorkingDirectory = $diagRoot $sc.IconLocation = "$env:SystemRoot\System32\imageres.dll,$($h.Icon)" $sc.Save() Write-Ok "Desktop shortcut: $($h.Shortcut)" } } catch { Write-Bad "could not create shortcuts: $($_.Exception.Message)" } } # ----------------------------------------------------------------------------- # Claude filesystem access # ----------------------------------------------------------------------------- # Wires the MCP filesystem server into Claude Desktop so he can ask it to look # at a broken OBS scene collection or a game config instead of describing it # down the phone. # # SCOPE THIS TIGHTLY, and specifically do NOT hand it %APPDATA% wholesale. # That folder holds browser profiles, saved sessions and the 1Password data -- # the exact session tokens the streaming checklist is about protecting. The # server can write inside whatever it is given, not just read, so every extra # directory is both a privacy and a blast-radius decision. # # The other reason to keep it narrow: anything he pastes into Claude is input. # If he pastes a config off a forum, or a "sponsor" document, that text is # acting with whatever file access this grants. if ((Want "claudefs")) { Write-Step "Claude filesystem access" if (-not $ClaudeDirs -or $ClaudeDirs.Count -eq 0) { $ClaudeDirs = @( "$env:APPDATA\obs-studio" # scenes, profiles, logs "$env:USERPROFILE\Diagnostics" # collect-diagnostics.ps1 output "$env:USERPROFILE\Downloads" [Environment]::GetFolderPath("Desktop") [Environment]::GetFolderPath("MyDocuments") ) } foreach ($d in $ClaudeDirs) { Write-Host " allow: $d" } $claudeCfgDir = "$env:APPDATA\Claude" $claudeCfg = "$claudeCfgDir\claude_desktop_config.json" if ($DryRun) { Write-Host " DRY would merge a filesystem server into $claudeCfg" } else { New-Item -Path $claudeCfgDir -ItemType Directory -Force | Out-Null # Merge, never clobber -- he may add his own servers later. $cfg = $null if (Test-Path $claudeCfg) { # Only ever back up the true original. -Force here would overwrite # the pristine backup with our own output on the second run. if (-not (Test-Path "$claudeCfg.bak")) { Copy-Item $claudeCfg "$claudeCfg.bak" } try { $cfg = Get-Content $claudeCfg -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-Bad "existing config is not valid JSON -- kept it at $claudeCfg.bak" $cfg = $null } } if ($null -eq $cfg) { $cfg = [pscustomobject]@{} } if (-not $cfg.PSObject.Properties['mcpServers']) { $cfg | Add-Member -NotePropertyName mcpServers -NotePropertyValue ([pscustomobject]@{}) } # npx.cmd rather than npx: the bare name resolves inconsistently when a # GUI app spawns it on Windows. $fsServer = [pscustomobject]@{ command = "npx.cmd" args = @("-y", "@modelcontextprotocol/server-filesystem") + $ClaudeDirs } if ($cfg.mcpServers.PSObject.Properties['filesystem']) { $cfg.mcpServers.filesystem = $fsServer } else { $cfg.mcpServers | Add-Member -NotePropertyName filesystem -NotePropertyValue $fsServer } $cfg | ConvertTo-Json -Depth 10 | Set-Content -Path $claudeCfg -Encoding UTF8 Write-Ok "filesystem server written to $claudeCfg" Write-Host " Claude Desktop must be fully quit and reopened to load it." Write-Host " Verify: ask Claude to list the files on the Desktop." } } # ----------------------------------------------------------------------------- # Streaming checklist # ----------------------------------------------------------------------------- # The parts a script cannot do for him. if ($Streaming) { Write-Step "Streaming checklist" $encoder = if ($gpus -match "NVIDIA") { "NVENC (Output > Encoder). Check whether your platform takes AV1 ingest -- on a 40/50-series card it looks clearly better at the same bitrate" } elseif ($gpus -match "AMD|Radeon") { "AMD HW AVC/AV1" } elseif ($gpus -match "Intel") { "QuickSync" } else { "x264 -- no hardware encoder detected, expect a CPU hit" } $checkLines = @( "Go-live checklist" "generated $(Get-Date -Format 'yyyy-MM-dd') by setup-gaming-box.ps1" "" "ACCOUNTS -- do these first" " [ ] Turn on 2FA for Twitch/YouTube, Discord, Steam, and email." " Same password twice anywhere = start over with 1Password." " [ ] 1Password generates the 2FA codes too -- put them there as you go," " so the codes and the passwords live in one place you actually open." " [ ] Install the 1Password browser extension when the app offers it." " Autofill is the whole point; without it nobody keeps using this." " [ ] Put the stream key in 1Password. Never on screen, never in a Discord DM." " [ ] If the key ever leaks: reset it from the dashboard, it is one click." "" "OBS" " [ ] Run the auto-config wizard on first launch." " [ ] Encoder: $encoder" " [ ] Bitrate: 6000 kbps at 1080p60 is a safe starting point. Check your" " platform's current limits and your own upload speed before pushing it." " [ ] Upload headroom: you want roughly 1.5x your bitrate in real upload." " Run a speed test; if upload is under 10 Mbps, drop to 936p or 720p60." " [ ] Enable the Replay Buffer -- that is how you clip your own highlights." " [ ] Scenes worth having: Starting Soon / Game / BRB / Ending." " [ ] Do a private or unlisted test stream before the first real one." "" "AUDIO" " [ ] Mic levels: peak around -12 dB, never touching 0." " [ ] Audio ducking is already disabled by this script." " [ ] VoiceMeeter only if you need to split game/mic/Discord onto separate" " tracks. If you do not know that you need it, you do not need it yet." "" "WHEN THE STREAM LOOKS BAD -- IN THIS ORDER" " Do not start by pinging things. Start by finding out which of the three" " possible problems you have, because they have different fixes." "" " 1. OBS STATS DOCK. View > Docks > Stats. Leave it open. It separates:" " Dropped frames (network) -> the connection to the ingest server" " Skipped frames (encoding) -> the encoder cannot keep up" " Rendering lag -> the GPU is busy, usually the game" " Nothing else matters until you know which of these is moving." "" " 2. OBS LOG ANALYZER. obsproject.com/tools/analyzer -- paste your OBS" " log, it flags misconfiguration and problem hardware for you. This is" " the first thing the OBS support forums will ask you for anyway." " Your last three logs are in the Diagnostics folder after running" " Collect Diagnostics." "" " 3. TWITCH INSPECTOR (inspector.twitch.tv). Streams a test to Twitch and" " reports whether YOUR bitrate was stable getting there. This is the" " one that settles arguments about whose fault it is." "" " 4. WinMTR (free) or PingPlotter (paid, prettier). Continuous traceroute" " showing packet loss PER HOP. If hop 4 is losing 20% and it is your" " ISPs router, that is the screenshot you send them. A plain ping" " cannot show you this." "" " 5. BUFFERBLOAT TEST -- waveform.com/tools/bufferbloat. If your latency" " is fine idle but spikes hard while streaming or downloading, that is" " bufferbloat, and it is why your game pings 200ms the moment you go" " live. The fix is QoS / SQM on the ROUTER, not on this PC. Most" " consumer routers can do it; it is worth the twenty minutes." "" " 6. Monitor Connection (Desktop shortcut). Logs gateway vs internet over" " time to a CSV. Use it for the intermittent stuff the one-shot tests" " above keep missing, then hand the CSV to Claude." "" "WHEN WINDOWS ASKS ABOUT THE FIREWALL" " Games pop a 'Windows Defender Firewall has blocked some features'" " dialog on first launch. Tick PRIVATE. Never tick Public." " Private means your home network. Public means the coffee shop, the" " hotel, the con hall -- and anything you allow there is allowed for" " everyone else on that network too." " If you tick the wrong one: Windows Security > Firewall & network" " protection > Allow an app. It is a checkbox, not a reinstall." "" "BEFORE EVERY STREAM" " [ ] Windows notifications off (Do Not Disturb) -- they show on stream." " [ ] Close anything with your real name, address, or email visible." " [ ] Check the preview before going live, not after." "" "THE SCAM YOU WILL ACTUALLY GET" " Streamers get 'sponsorship' emails and Discord DMs constantly. The offer" " looks real, the attachment or download link is an infostealer, and it takes" " your browser session tokens -- which walks straight past 2FA and takes the" " channel with it." " Rules that hold up:" " - Never run a .exe/.scr someone sent you to 'review our game'." " - A real sponsor does not need you to download anything to sign a deal." " - Password-protected zips exist to hide from antivirus. That is the point." " - If you are tempted anyway, run it on something that is not this box." ) $checkPath = Join-Path $refDir "streaming-checklist.txt" if ($DryRun) { Write-Host " DRY would write $checkPath" } else { $checkLines -join "`r`n" | Set-Content -Path $checkPath -Encoding UTF8 Write-Ok $checkPath } } # ----------------------------------------------------------------------------- # VTuber guide # ----------------------------------------------------------------------------- # Almost none of this is on winget -- VRoid/VTube Studio/Warudo are Steam, and # VSeeFace/VNyan are direct downloads. So this writes a guide instead of # pretending to install things. Names are given without store IDs on purpose: # look them up rather than trusting a link pasted from memory. if ($VTuber) { Write-Step "VTuber guide" $vtLines = @( "VTuber setup" "generated $(Get-Date -Format 'yyyy-MM-dd') by setup-gaming-box.ps1" "" "COMMISSIONING THE MODEL" "" " ART AND RIGGING ARE TWO DIFFERENT JOBS." " This is the thing first-timers get wrong and it is expensive to fix." " An illustrator draws the character. A rigger makes it move. Plenty of" " artists do not rig, and art that was not drawn FOR rigging -- every part" " on its own layer, pieces extended behind whatever overlaps them -- has to" " be redrawn before a rigger can touch it." " So: either commission someone who does both, or line up the rigger first" " and have the artist deliver to that rigger's layer spec. Ask the artist" " outright: do you rig, and have you rigged for VTubing before?" "" " WHAT TO SPECIFY UP FRONT, IN WRITING" " - Live2D or 3D VRM. Live2D is the classic look and is driven by VTube" " Studio. VRM is driven by VSeeFace / VNyan / Warudo." " - Deliverable files, by name. Live2D: the VTube Studio-ready folder," " not just a picture. 3D: a .vrm that opens in your tracker." " - Whether you get the layered source files (PSD / project). You want" " them. Without them, every future tweak means going back to them." " - Commercial rights, spelled out: monetised streams, thumbnails," " emotes, merch. 'Personal use' licences will bite you later." " - Expression toggles and hotkeys you want: blush, angry, tears," " glasses on/off, props. Each one is work; agree the list now." " - Revisions included, and the delivery date." "" " WRITING THE BRIEF" " Everything above is a spec, and artists much prefer a spec to a vibe." " Claude is on this machine -- paste that list in, tell it what your" " character looks like and what you stream, and have it turn the whole" " thing into a brief you can actually send. Read it before you send it." "" " TELL THEM IT IS A GAMING CORNER OVERLAY." " You are streaming games with the avatar small in a corner, not a" " full-screen talking head. Fine detail and tiny accessories disappear at" " that size. A good artist designs differently once they know this --" " stronger silhouette, readable expressions, less filigree." "" " NOT GETTING BURNED" " - Pay through a platform that has escrow and a dispute process." " PayPal friends-and-family has no recourse. None." " - Ask for rigged demo VIDEO, not static illustrations. Anyone can post" " pretty art; you are buying movement." " - Ask for work-in-progress files. It filters resold and AI-scraped" " portfolios." " - Half up front, half on delivery is normal. Everything up front to a" " stranger with no platform behind them is not." "" " BUDGET AND QUEUE, ROUGHLY" " Premade models sell for a small fraction of a custom commission and are" " instant. Full custom art plus rigging is a serious spend and the queue is" " measured in weeks to months, not days. Check current rates yourself --" " this market moves and anything quoted here goes stale." "" " WHILE YOU WAIT -- DO NOT JUST WAIT" " Build a throwaway avatar in VRoid Studio this week and run your whole" " pipeline on it: tracking, lighting, hotkeys, the OBS scene, Spout2," " a test stream. All of that work is identical for the real model, so when" " the commission lands it is a drop-in swap. The alternative is waiting two" " months and THEN starting to learn, on the model you paid for." "" " WHEN IT ARRIVES, CHECK BEFORE YOU SIGN OFF" " - It loads in your actual tracker, on this machine." " - Every expression hotkey you paid for fires." " - It still reads clearly shrunk into a stream corner." " - You know what it does when tracking drops." "" "SOFTWARE -- search these by name, do not trust a pasted link" " (VRoid is for the placeholder; the tracker is whichever matches the" " model you commissioned -- VTube Studio for Live2D, the rest for VRM.)" " VRoid Studio -- build a VRM avatar, free" " VSeeFace -- free VRM tracker, webcam or iPhone, the usual first stop" " VNyan -- free VRM tracker, better props and stream integrations" " Warudo -- 3D, heavier and prettier, on Steam" " VTube Studio -- the Live2D standard, also handles VRM" "" "TRACKING -- this is where the quality actually comes from" " A webcam is the floor. Any 1080p one works; lighting matters far more" " than which camera you bought." " An iPhone with Face ID beats every webcam, because ARKit gives real" " blendshapes. Bridge apps send it over wifi to VSeeFace or VTube Studio." " One soft light in front of your face. Nothing bright behind you." " Hand tracking (Leap Motion / Ultraleap) is a later problem. Skip it." "" "OBS CAPTURE" " Install the Spout2 plugin for OBS. It hands the avatar to OBS with real" " transparency -- no green screen, no chroma fringing. Ten minutes, worth it." " Fallback: Window Capture of the tracker plus a chroma key filter on a" " solid background colour." "" "GAMING WITH THE AVATAR ON SCREEN -- your actual setup" " Scene layout: Game Capture on the bottom layer, avatar on top via Spout2" " so it floats transparent in a corner. Do not stack two window captures;" " that is where the framerate goes." " Run games BORDERLESS WINDOWED, not fullscreen exclusive. Exclusive" " fullscreen fights overlays and makes alt-tab a five second black screen" " mid-conversation. Borderless costs you nothing you will notice." " Bind tracker hotkeys -- recentre tracking, toggle expressions -- so you can" " fix the avatar without alt-tabbing out of a match." " Your avatar looks at the camera, not at the game. Viewers clock the" " mismatch fast. Some trackers can bias the head toward the screen; worth" " ten minutes of fiddling once." " Competitive shooters: check the anti-cheat before you add overlays." " Tracker plus OBS plus an aggressive anti-cheat is a known bad night." "" "YOUR 5070 Ti SPECIFICALLY" " 16 GB of VRAM. Avatar plus game plus OBS at once is a non-issue; do not" " let anyone sell you a second PC for streaming." " Encode with NVENC. If the platform accepts AV1 ingest, use it." " If tracking stutters, it is not the card -- trackers lean on the CPU and" " the webcam's framerate. Check lighting and camera FPS before anything else." " NVIDIA Broadcast: use it for microphone noise removal. Its background" " removal is pointless when your background is an avatar." "" "BEFORE GOING LIVE" " The avatar hides your face. It does not hide your desktop, your tabs," " or your notifications. Same rules as the streaming checklist." " Find out what your model does when tracking drops out -- most T-pose or" " go glassy-eyed. Better to see it now than on stream." ) $vtPath = Join-Path $refDir "vtuber-setup.txt" if ($DryRun) { Write-Host " DRY would write $vtPath" } else { $vtLines -join "`r`n" | Set-Content -Path $vtPath -Encoding UTF8 Write-Ok $vtPath } } # ----------------------------------------------------------------------------- # Help page # ----------------------------------------------------------------------------- # The one thing on the Desktop he is actually meant to open. Standalone HTML, # so it works with the internet down -- which is exactly when he needs the # troubleshooting half of it. Write-Step "Help page" $helpSrc = Join-Path $PSScriptRoot "help.html" $helpDst = Join-Path ([Environment]::GetFolderPath("Desktop")) "START HERE.html" if (-not (Want "helppage")) { Write-Skip "skipped by choice" } elseif (-not (Test-Path $helpSrc)) { Write-Bad "help.html not found next to this script -- skipping" $script:Failures += "help page (file missing)" } elseif ($DryRun) { Write-Host " DRY would copy help.html to $helpDst" } else { Copy-Item $helpSrc $helpDst -Force Write-Ok $helpDst } # ----------------------------------------------------------------------------- # Summary # ----------------------------------------------------------------------------- Write-Step "Summary" Write-Host " installed: $($script:Installed.Count) already there: $($script:Skipped.Count) failed: $($script:Failures.Count)" if ($script:Manual.Count -gt 0) { Write-Host "`n Download these by hand -- not available through winget:" -ForegroundColor Cyan foreach ($m in $script:Manual) { Write-Host " - $($m.Name)" Write-Host " $($m.Url)" } Write-Host " The GPU driver app is the one that actually matters. Do it first." } if ($script:Failures.Count -gt 0) { Write-Host "`n These need a hand:" -ForegroundColor Yellow foreach ($f in $script:Failures) { Write-Host " - $f" } Write-Host " Check the id with: winget search " } Write-Host "`n Next, by hand:" Write-Host " 1. Reboot (drivers)." Write-Host " 2. Sign in to Steam + Discord." Write-Host " 3. Make Chrome default: Settings > Apps > Default apps > Chrome" Write-Host " > 'Set default'. Windows 11 blocks scripting this, sorry." Write-Host " 4. Set up 1Password (+ the browser extension), then 2FA everywhere." if (-not $SkipClaudeFs) { Write-Host " 5. Sign in to Claude, quit it FULLY, reopen it, then ask it to" Write-Host " list the Desktop to confirm file access works." } Write-Host "" Write-Host " Then hand him the machine and say one thing:" Write-Host " open 'START HERE' on the Desktop when anything goes wrong." Write-Host " Everything else is in there." Write-Host "" Write-Host " Long-form reference (for you and for Claude, not for him):" Write-Host " Documents\Rig Reference\" Write-Host "" Write-Host " Full log of this run:" Write-Host " $LogPath" Stop-Log Write-Host ""