Created
March 6, 2026 21:29
-
-
Save inxomnyaa/edf17f85ad9793a17f441827cd2a48b7 to your computer and use it in GitHub Desktop.
Steam Shortcut Icon Fixer - Fixes your blank icons in the start menu or folders
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Steam Shortcut Icon Fixer | |
| Write-Host "Steam Shortcut Icon Fixer" -ForegroundColor Cyan | |
| Write-Host ("=" * 60) | |
| Write-Host "" | |
| # Use environment variables for paths | |
| $defaultShortcutsPath = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Steam" | |
| $defaultSteamPath = "C:\Program Files (x86)\Steam" | |
| # Prompt for paths | |
| $shortcutsPath = Read-Host "Enter shortcuts folder path (default: $defaultShortcutsPath)" | |
| if ([string]::IsNullOrWhiteSpace($shortcutsPath)) { | |
| $shortcutsPath = $defaultShortcutsPath | |
| } | |
| $steamInstallPath = Read-Host "Enter Steam installation path (default: $defaultSteamPath)" | |
| if ([string]::IsNullOrWhiteSpace($steamInstallPath)) { | |
| $steamInstallPath = $defaultSteamPath | |
| } | |
| Write-Host "" | |
| Write-Host "Shortcuts folder: $shortcutsPath" -ForegroundColor Gray | |
| Write-Host "Steam install path: $steamInstallPath" -ForegroundColor Gray | |
| Write-Host "" | |
| # Validate paths | |
| if (-not (Test-Path $shortcutsPath)) { | |
| Write-Host "Error: Shortcuts folder not found!" -ForegroundColor Red | |
| exit | |
| } | |
| if (-not (Test-Path $steamInstallPath)) { | |
| Write-Host "Error: Steam installation path not found!" -ForegroundColor Red | |
| exit | |
| } | |
| # Parse libraryfolders.vdf to get all Steam library paths | |
| $libraryVdfPath = Join-Path $steamInstallPath "steamapps\libraryfolders.vdf" | |
| $steamLibraries = @($steamInstallPath) | |
| if (Test-Path $libraryVdfPath) { | |
| $vdfContent = Get-Content -Path $libraryVdfPath -Raw | |
| # Extract paths from VDF file | |
| $matches = [regex]::Matches($vdfContent, '"path"\s+"([^"]+)"') | |
| foreach ($match in $matches) { | |
| $libPath = $match.Groups[1].Value -replace '\\\\', '\' | |
| if ($libPath -ne $steamInstallPath -and -not $steamLibraries.Contains($libPath)) { | |
| $steamLibraries += $libPath | |
| } | |
| } | |
| } | |
| Write-Host "Found $($steamLibraries.Count) Steam library location(s):" -ForegroundColor Yellow | |
| foreach ($lib in $steamLibraries) { | |
| Write-Host " • $lib" -ForegroundColor Gray | |
| } | |
| Write-Host "" | |
| # Build game ID to folder name mapping | |
| $gameIdMap = @{} | |
| foreach ($libPath in $steamLibraries) { | |
| $appManifestPath = Join-Path $libPath "steamapps" | |
| if (-not (Test-Path $appManifestPath)) { | |
| continue | |
| } | |
| # Read all appmanifest_*.acf files | |
| $manifests = Get-ChildItem -Path $appManifestPath -Filter "appmanifest_*.acf" -ErrorAction SilentlyContinue | |
| foreach ($manifest in $manifests) { | |
| $manifestContent = Get-Content -Path $manifest.FullName -Raw | |
| # Extract appid | |
| if ($manifestContent -match '"appid"\s+"(\d+)"') { | |
| $appId = $matches[1] | |
| # Extract installdir | |
| if ($manifestContent -match '"installdir"\s+"([^"]+)"') { | |
| $installDir = $matches[1] | |
| $gameIdMap[$appId] = @{ | |
| FolderName = $installDir | |
| LibraryPath = $libPath | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Write-Host "Mapped $($gameIdMap.Count) game(s) from app manifests." -ForegroundColor Yellow | |
| Write-Host "" | |
| # Function to check if this is a mod (has gameinfo.txt) | |
| function Test-IsMod { | |
| param([string]$GameFolder) | |
| $gameInfoFiles = Get-ChildItem -Path $GameFolder -Filter "gameinfo.txt" -Recurse -ErrorAction SilentlyContinue | |
| return $gameInfoFiles.Count -gt 0 | |
| } | |
| # Function to find icon from gameinfo.txt | |
| function Find-ModIcon { | |
| param([string]$GameFolder) | |
| # Look for gameinfo.txt recursively | |
| $gameInfoFiles = Get-ChildItem -Path $GameFolder -Filter "gameinfo.txt" -Recurse -ErrorAction SilentlyContinue | |
| foreach ($gameInfoFile in $gameInfoFiles) { | |
| $gameInfoContent = Get-Content -Path $gameInfoFile.FullName -Raw | |
| # Extract icon path from gameinfo.txt | |
| if ($gameInfoContent -match 'icon\s+"([^"]+)"') { | |
| $iconPath = $matches[1] | |
| $gameInfoDir = $gameInfoFile.Directory.FullName | |
| # Try icon extensions (skip .tga and .bmp as they don't work well) | |
| $iconExtensions = @('.ico', '.png') | |
| foreach ($ext in $iconExtensions) { | |
| $fullIconPath = Join-Path $gameInfoDir ($iconPath + $ext) | |
| if (Test-Path $fullIconPath) { | |
| return $fullIconPath | |
| } | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| # Function to find custom icon in resource folder | |
| function Find-ResourceIcon { | |
| param([string]$GameFolder) | |
| # Look for resource folders | |
| $resourceFolders = Get-ChildItem -Path $gameFolder -Filter "resource" -Directory -Recurse -ErrorAction SilentlyContinue | |
| # Only look for .ico and .png (skip .tga and .bmp) | |
| $iconNames = @('game.ico', 'icon.ico', 'game-icon.ico', '*.ico', 'game.png', 'icon.png') | |
| foreach ($resourceFolder in $resourceFolders) { | |
| foreach ($iconName in $iconNames) { | |
| $fullPath = Join-Path $resourceFolder.FullName $iconName | |
| if (Test-Path $fullPath) { | |
| return $fullPath | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| # Initialize counters | |
| $fixed = @() | |
| $failed = @() | |
| # Get all .url files | |
| $urlFiles = Get-ChildItem -Path $shortcutsPath -Filter "*.url" -ErrorAction SilentlyContinue | |
| if ($urlFiles.Count -eq 0) { | |
| Write-Host "No .url files found in $shortcutsPath" -ForegroundColor Yellow | |
| exit | |
| } | |
| Write-Host "Found $($urlFiles.Count) shortcut(s). Processing..." -ForegroundColor Yellow | |
| Write-Host "" | |
| foreach ($file in $urlFiles) { | |
| $gameName = $file.BaseName | |
| $shortcutPath = $file.FullName | |
| # Read the .url file to extract game ID | |
| $urlContent = Get-Content -Path $shortcutPath -Raw | |
| # Extract game ID from steam://rungameid/XXXXX | |
| if ($urlContent -match 'steam://rungameid/(\d+)') { | |
| $gameId = $matches[1] | |
| # Look up the game in our map | |
| if ($gameIdMap.ContainsKey($gameId)) { | |
| $gameInfo = $gameIdMap[$gameId] | |
| # Chain Join-Path calls properly | |
| $gameFolder = Join-Path (Join-Path $gameInfo.LibraryPath "steamapps") "common" | |
| $gameFolder = Join-Path $gameFolder $gameInfo.FolderName | |
| if (Test-Path $gameFolder) { | |
| $iconPath = $null | |
| $isMod = Test-IsMod -GameFolder $gameFolder | |
| # Only use mod detection if it's actually a mod | |
| if ($isMod) { | |
| # Priority 1: Check for mod icon in gameinfo.txt | |
| $iconPath = Find-ModIcon -GameFolder $gameFolder | |
| # Priority 2: Check for resource folder icons | |
| if (-not $iconPath) { | |
| $iconPath = Find-ResourceIcon -GameFolder $gameFolder | |
| } | |
| } | |
| # Priority 3: Find the main .exe with priority matching | |
| if (-not $iconPath) { | |
| $allExes = Get-ChildItem -Path $gameFolder -Filter "*.exe" -Recurse | | |
| Where-Object { $_.Name -notmatch "unins|setup|launcher|vcredist|dotnet|redist|helper|crash|report|config|benchmark" } | |
| $exeFile = $null | |
| # Priority 3a: Root folder only (no subfolders) | |
| if (-not $exeFile) { | |
| $exeFile = $allExes | Where-Object { $_.DirectoryName -eq $gameFolder } | Select-Object -First 1 | |
| } | |
| # Priority 3b: Match game folder name | |
| if (-not $exeFile) { | |
| $exeFile = $allExes | Where-Object { $_.BaseName -match [regex]::Escape($gameInfo.FolderName) } | Select-Object -First 1 | |
| } | |
| ## TODO improve the following, portal revolution and reloaded fail | |
| # Priority 3c: Common main executable names | |
| if (-not $exeFile) { | |
| $exeFile = $allExes | Where-Object { $_.BaseName -match "^(game|main|start|run|play)" } | Select-Object -First 1 | |
| } | |
| # Priority 3d: Largest exe (usually the main one) | |
| if (-not $exeFile) { | |
| $exeFile = $allExes | Sort-Object -Property Length -Descending | Select-Object -First 1 | |
| } | |
| if ($exeFile) { | |
| $iconPath = $exeFile.FullName | |
| } | |
| } | |
| if ($iconPath) { | |
| # Update the .url file with icon path | |
| # Remove any existing IconFile line | |
| $newUrlContent = $urlContent -replace '\r?\nIconFile=.*', '' | |
| # Add the new IconFile line with proper escaping for spaces | |
| $newUrlContent = $newUrlContent.TrimEnd() + "`r`nIconFile=$iconPath`r`n" | |
| Set-Content -Path $shortcutPath -Value $newUrlContent -Force | |
| $fixed += @{ | |
| Name = $gameName | |
| GameId = $gameId | |
| IconPath = $iconPath | |
| } | |
| } else { | |
| $failed += @{ | |
| Name = $gameName | |
| GameId = $gameId | |
| Reason = "No icon or .exe found" | |
| FolderPath = $gameFolder | |
| } | |
| } | |
| } else { | |
| $failed += @{ | |
| Name = $gameName | |
| GameId = $gameId | |
| Reason = "Game folder not found" | |
| FolderPath = $gameFolder | |
| } | |
| } | |
| } else { | |
| $failed += @{ | |
| Name = $gameName | |
| GameId = $gameId | |
| Reason = "Game ID not found in Steam app manifests" | |
| } | |
| } | |
| } else { | |
| $failed += @{ | |
| Name = $gameName | |
| Reason = "Could not extract game ID from URL" | |
| } | |
| } | |
| } | |
| # Display summary BEFORE clearing cache | |
| Write-Host "" | |
| Write-Host ("=" * 60) | |
| Write-Host "SUMMARY" -ForegroundColor Cyan | |
| Write-Host ("=" * 60) | |
| if ($fixed.Count -gt 0) { | |
| Write-Host "" | |
| Write-Host "✓ FIXED ($($fixed.Count)):" -ForegroundColor Green | |
| foreach ($item in $fixed) { | |
| Write-Host " • $($item.Name) (ID: $($item.GameId))" -ForegroundColor Green | |
| Write-Host " → $($item.IconPath)" -ForegroundColor DarkGreen | |
| } | |
| } | |
| if ($failed.Count -gt 0) { | |
| Write-Host "" | |
| Write-Host "✗ FAILED ($($failed.Count)):" -ForegroundColor Red | |
| foreach ($item in $failed) { | |
| Write-Host " • $($item.Name)" -ForegroundColor Red | |
| if ($item.GameId) { | |
| Write-Host " ID: $($item.GameId)" -ForegroundColor DarkRed | |
| } | |
| Write-Host " Reason: $($item.Reason)" -ForegroundColor DarkRed | |
| if ($item.FolderPath) { | |
| Write-Host " Expected path: $($item.FolderPath)" -ForegroundColor DarkRed | |
| } | |
| } | |
| } | |
| Write-Host "" | |
| Write-Host "Total: $($fixed.Count) fixed, $($failed.Count) failed" -ForegroundColor Cyan | |
| Write-Host "" | |
| # Prompt before clearing cache | |
| $proceed = Read-Host "Clear icon cache and restart Explorer? (Y/n)" | |
| if ($proceed -ne "n") { | |
| Write-Host "" | |
| Write-Host "Clearing icon cache and restarting Explorer..." -ForegroundColor Cyan | |
| Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue | |
| Start-Sleep -Seconds 1 | |
| Remove-Item -Path "$env:LOCALAPPDATA\IconCache.db" -Force -ErrorAction SilentlyContinue | |
| Start-Process explorer | |
| Start-Sleep -Seconds 2 | |
| # Open the shortcuts folder | |
| Write-Host "Opening shortcuts folder..." -ForegroundColor Cyan | |
| Invoke-Item $shortcutsPath | |
| } | |
| Write-Host "Done!" -ForegroundColor Green |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment