diff --git a/.gitignore b/.gitignore
index 7691a66..2beebd2 100755
--- a/.gitignore
+++ b/.gitignore
@@ -9,5 +9,6 @@ __pycache__/
.Trashes
ehthumbs.db
Thumbs.db
+desktop.ini
.~lock*
diff --git a/Audit-RepoHealth.ps1 b/Audit-RepoHealth.ps1
new file mode 100644
index 0000000..e7ae16a
--- /dev/null
+++ b/Audit-RepoHealth.ps1
@@ -0,0 +1,320 @@
+#!/usr/bin/env pwsh
+<#
+.SYNOPSIS
+ Audits a Git repository for Windows-incompatible and system files.
+
+.DESCRIPTION
+ Scans the repo for problematic files:
+ - Windows system metadata (desktop.ini, Thumbs.db, etc.)
+ - Reserved Windows filenames (CON, NUL, PRN, AUX, LPT1-9, COM1-9)
+ - Untracked system files that shouldn't be committed
+ - Symlink anomalies
+
+.PARAMETER RepoPath
+ Path to the Git repository. Defaults to current working directory.
+
+.PARAMETER FixMode
+ If specified, automatically removes problematic files and updates .gitignore.
+
+.EXAMPLE
+ .\Audit-RepoHealth.ps1
+ .\Audit-RepoHealth.ps1 -RepoPath "C:\repos\my-project"
+ .\Audit-RepoHealth.ps1 -RepoPath "." -FixMode
+
+.NOTES
+ Author: GitHub Copilot
+ Date: December 2025
+#>
+
+param(
+ [string]$RepoPath = (Get-Location).Path,
+ [switch]$FixMode = $false
+)
+
+# Define problematic files and patterns
+$WindowsSystemFiles = @(
+ "desktop.ini"
+ "Thumbs.db"
+ "ehthumbs.db"
+ ".DS_Store"
+ "._*"
+ ".Trashes"
+ ".Spotlight-V100"
+ "Zone.Identifier"
+ ".fseventsd"
+)
+
+$ReservedNames = @(
+ "CON", "PRN", "AUX", "NUL"
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9"
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
+)
+
+function Test-IsGitRepo {
+ param([string]$Path)
+
+ try {
+ Push-Location $Path
+ $null = git rev-parse --git-dir 2>$null
+ $result = $LASTEXITCODE -eq 0
+ Pop-Location
+ return $result
+ }
+ catch {
+ return $false
+ }
+}
+
+function Get-GitTrackedFiles {
+ param([string]$Path)
+
+ try {
+ Push-Location $Path
+ $files = @(git ls-files 2>$null)
+ Pop-Location
+ return $files
+ }
+ catch {
+ return @()
+ }
+}
+
+function Get-UntrackedFiles {
+ param([string]$Path)
+
+ try {
+ Push-Location $Path
+ $files = @(git ls-files --others --exclude-standard 2>$null)
+ Pop-Location
+ return $files
+ }
+ catch {
+ return @()
+ }
+}
+
+function Find-ProblematicFiles {
+ param(
+ [string]$Path,
+ [string[]]$Patterns,
+ [string]$SearchType = "untracked" # "tracked", "untracked", "all"
+ )
+
+ $problematic = @()
+
+ if ($SearchType -in @("tracked", "all")) {
+ $tracked = Get-GitTrackedFiles -Path $Path
+ foreach ($pattern in $Patterns) {
+ $matching = $tracked | Where-Object { $_ -match $pattern }
+ $problematic += $matching
+ }
+ }
+
+ if ($SearchType -in @("untracked", "all")) {
+ $untracked = Get-UntrackedFiles -Path $Path
+ foreach ($pattern in $Patterns) {
+ $matching = $untracked | Where-Object { $_ -match $pattern }
+ $problematic += $matching
+ }
+ }
+
+ return $problematic | Select-Object -Unique
+}
+
+function Test-ReservedName {
+ param([string]$Filename)
+
+ $baseName = [System.IO.Path]::GetFileNameWithoutExtension($Filename)
+ return $baseName -in $ReservedNames
+}
+
+function Find-ReservedNames {
+ param([string]$Path)
+
+ $problematic = @()
+
+ try {
+ Push-Location $Path
+ $allFiles = Get-ChildItem -Recurse -Force -ErrorAction SilentlyContinue |
+ Where-Object { -not $_.PSIsContainer }
+
+ foreach ($file in $allFiles) {
+ $baseName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
+ if (Test-ReservedName -Filename $file.Name) {
+ $problematic += $file.FullName
+ }
+ }
+
+ Pop-Location
+ }
+ catch {
+ Write-Warning "Error scanning for reserved names: $_"
+ }
+
+ return $problematic
+}
+
+function Remove-ProblematicFile {
+ param(
+ [string]$FilePath,
+ [string]$RepoPath
+ )
+
+ try {
+ $fullPath = Join-Path $RepoPath $FilePath
+
+ if (Test-Path $fullPath) {
+ # Try to remove from git first if tracked
+ Push-Location $RepoPath
+ $tracked = @(git ls-files) | Where-Object { $_ -eq $FilePath }
+ if ($tracked) {
+ git rm --cached $FilePath 2>$null | Out-Null
+ Write-Host " ā Untracked from git: $FilePath"
+ }
+ Pop-Location
+
+ # Remove local file
+ Remove-Item $fullPath -Force -ErrorAction SilentlyContinue
+ Write-Host " ā Deleted: $FilePath"
+ return $true
+ }
+ }
+ catch {
+ Write-Warning " ā Failed to remove $FilePath : $_"
+ return $false
+ }
+
+ return $true
+}
+
+function Update-Gitignore {
+ param(
+ [string]$RepoPath,
+ [string[]]$Entries
+ )
+
+ $gitignorePath = Join-Path $RepoPath ".gitignore"
+ $existingContent = @()
+
+ if (Test-Path $gitignorePath) {
+ $existingContent = @(Get-Content $gitignorePath)
+ }
+
+ $toAdd = @()
+ foreach ($entry in $Entries) {
+ if ($existingContent -notcontains $entry) {
+ $toAdd += $entry
+ }
+ }
+
+ if ($toAdd.Count -gt 0) {
+ Add-Content -Path $gitignorePath -Value "" -ErrorAction SilentlyContinue
+ Add-Content -Path $gitignorePath -Value "# System files (auto-added by Audit-RepoHealth.ps1)" -ErrorAction SilentlyContinue
+ Add-Content -Path $gitignorePath -Value $toAdd -ErrorAction SilentlyContinue
+ Write-Host "ā Updated .gitignore with $($ toAdd.Count) entries"
+ return $true
+ }
+
+ return $false
+}
+
+# ===== MAIN SCRIPT =====
+
+Write-Host "`nš Git Repository Health Audit`n" -ForegroundColor Cyan
+Write-Host "Repository: $RepoPath`n"
+
+# Verify it's a git repo
+if (-not (Test-IsGitRepo -Path $RepoPath)) {
+ Write-Host "ā Error: Not a valid Git repository!" -ForegroundColor Red
+ exit 1
+}
+
+# Build regex patterns
+$patterns = $WindowsSystemFiles | ForEach-Object {
+ [regex]::Escape($_) -replace '\\\*', '.*'
+}
+
+# ===== SCAN FOR TRACKED SYSTEM FILES =====
+Write-Host "š Scanning for tracked system files..." -ForegroundColor Yellow
+$trackedProblematic = Find-ProblematicFiles -Path $RepoPath -Patterns $patterns -SearchType "tracked"
+
+if ($trackedProblematic.Count -gt 0) {
+ Write-Host "`nā ļø Found tracked system files:" -ForegroundColor Red
+ $trackedProblematic | ForEach-Object { Write-Host " - $_" }
+
+ if ($FixMode) {
+ Write-Host "`nš§ Removing tracked system files..." -ForegroundColor Yellow
+ foreach ($file in $trackedProblematic) {
+ Remove-ProblematicFile -FilePath $file -RepoPath $RepoPath
+ }
+ }
+}
+else {
+ Write-Host "ā No tracked system files found" -ForegroundColor Green
+}
+
+# ===== SCAN FOR UNTRACKED SYSTEM FILES =====
+Write-Host "`nš Scanning for untracked system files..." -ForegroundColor Yellow
+$untrackedProblematic = Find-ProblematicFiles -Path $RepoPath -Patterns $patterns -SearchType "untracked"
+
+if ($untrackedProblematic.Count -gt 0) {
+ Write-Host "`nā ļø Found untracked system files:" -ForegroundColor Yellow
+ $untrackedProblematic | ForEach-Object { Write-Host " - $_" }
+
+ if ($FixMode) {
+ Write-Host "`nš§ Removing untracked system files..." -ForegroundColor Yellow
+ foreach ($file in $untrackedProblematic) {
+ $fullPath = Join-Path $RepoPath $file
+ if (Test-Path $fullPath) {
+ Remove-Item $fullPath -Force -ErrorAction SilentlyContinue
+ Write-Host " ā Deleted: $file"
+ }
+ }
+ }
+}
+else {
+ Write-Host "ā No untracked system files found" -ForegroundColor Green
+}
+
+# ===== SCAN FOR RESERVED WINDOWS NAMES =====
+Write-Host "`nš Scanning for reserved Windows filenames..." -ForegroundColor Yellow
+$reserved = Find-ReservedNames -Path $RepoPath
+
+if ($reserved.Count -gt 0) {
+ Write-Host "`nā ļø Found files with reserved Windows names:" -ForegroundColor Red
+ $reserved | ForEach-Object {
+ $relPath = $_.Replace($RepoPath, "").TrimStart("\")
+ Write-Host " - $relPath"
+ }
+
+ if ($FixMode) {
+ Write-Host "`nš§ These files should be renamed manually (Windows reserved names):`n" -ForegroundColor Yellow
+ $reserved | ForEach-Object {
+ $relPath = $_.Replace($RepoPath, "").TrimStart("\")
+ Write-Host " TODO: Rename $relPath"
+ }
+ }
+}
+else {
+ Write-Host "ā No reserved Windows filenames found" -ForegroundColor Green
+}
+
+# ===== UPDATE .GITIGNORE =====
+if ($FixMode -and ($trackedProblematic.Count -gt 0 -or $untrackedProblematic.Count -gt 0)) {
+ Write-Host "`nš Updating .gitignore..." -ForegroundColor Yellow
+ Update-Gitignore -RepoPath $RepoPath -Entries $WindowsSystemFiles
+}
+
+# ===== FINAL REPORT =====
+$totalIssues = $trackedProblematic.Count + $untrackedProblematic.Count + $reserved.Count
+
+Write-Host "`n" + ("=" * 60)
+if ($totalIssues -eq 0) {
+ Write-Host "ā
Repository is clean! No issues detected." -ForegroundColor Green
+}
+else {
+ Write-Host "ā ļø Found $totalIssues issue(s). Use -FixMode to remediate." -ForegroundColor Yellow
+}
+Write-Host "=" * 60 + "`n"
+
+exit $totalIssues
diff --git a/SkyboxManager.cs b/SkyboxManager.cs
new file mode 100644
index 0000000..2c2ce02
--- /dev/null
+++ b/SkyboxManager.cs
@@ -0,0 +1,165 @@
+using UnityEngine;
+using UnityEngine.Rendering;
+
+///
+/// SkyboxManager: Loads and manages skybox materials programmatically.
+/// Attach to any GameObject in your scene to use.
+///
+public class SkyboxManager : MonoBehaviour
+{
+ [System.Serializable]
+ public class SkyboxEntry
+ {
+ public string name;
+ public Material skyboxMaterial;
+ }
+
+ [SerializeField]
+ private SkyboxEntry[] availableSkyboxes;
+
+ [SerializeField]
+ private int defaultSkyboxIndex = 0;
+
+ private int currentSkyboxIndex = -1;
+
+ private void Start()
+ {
+ if (availableSkyboxes != null && availableSkyboxes.Length > 0)
+ {
+ SetSkybox(defaultSkyboxIndex);
+ }
+ else
+ {
+ Debug.LogWarning("No skyboxes assigned to SkyboxManager!");
+ }
+ }
+
+ ///
+ /// Sets the skybox by index.
+ ///
+ public void SetSkybox(int index)
+ {
+ if (availableSkyboxes == null || index < 0 || index >= availableSkyboxes.Length)
+ {
+ Debug.LogError($"Invalid skybox index: {index}");
+ return;
+ }
+
+ Material skyboxMaterial = availableSkyboxes[index].skyboxMaterial;
+ if (skyboxMaterial == null)
+ {
+ Debug.LogError($"Skybox at index {index} is null!");
+ return;
+ }
+
+ RenderSettings.skybox = skyboxMaterial;
+ DynamicGI.UpdateEnvironment();
+ currentSkyboxIndex = index;
+
+ Debug.Log($"Skybox changed to: {availableSkyboxes[index].name}");
+ }
+
+ ///
+ /// Sets the skybox by name.
+ ///
+ public void SetSkyboxByName(string skyboxName)
+ {
+ for (int i = 0; i < availableSkyboxes.Length; i++)
+ {
+ if (availableSkyboxes[i].name == skyboxName)
+ {
+ SetSkybox(i);
+ return;
+ }
+ }
+
+ Debug.LogError($"Skybox '{skyboxName}' not found!");
+ }
+
+ ///
+ /// Loads a skybox material from Resources folder.
+ /// Place your materials in: Assets/Resources/Skyboxes/
+ ///
+ public void LoadSkyboxFromResources(string resourcePath)
+ {
+ Material skyboxMaterial = Resources.Load(resourcePath);
+ if (skyboxMaterial == null)
+ {
+ Debug.LogError($"Could not load skybox from Resources: {resourcePath}");
+ return;
+ }
+
+ RenderSettings.skybox = skyboxMaterial;
+ DynamicGI.UpdateEnvironment();
+ Debug.Log($"Skybox loaded from Resources: {resourcePath}");
+ }
+
+ ///
+ /// Cycles to the next skybox.
+ ///
+ public void NextSkybox()
+ {
+ if (availableSkyboxes == null || availableSkyboxes.Length == 0)
+ return;
+
+ int nextIndex = (currentSkyboxIndex + 1) % availableSkyboxes.Length;
+ SetSkybox(nextIndex);
+ }
+
+ ///
+ /// Cycles to the previous skybox.
+ ///
+ public void PreviousSkybox()
+ {
+ if (availableSkyboxes == null || availableSkyboxes.Length == 0)
+ return;
+
+ int prevIndex = (currentSkyboxIndex - 1 + availableSkyboxes.Length) % availableSkyboxes.Length;
+ SetSkybox(prevIndex);
+ }
+
+ ///
+ /// Gets the name of the currently active skybox.
+ ///
+ public string GetCurrentSkyboxName()
+ {
+ if (currentSkyboxIndex >= 0 && currentSkyboxIndex < availableSkyboxes.Length)
+ {
+ return availableSkyboxes[currentSkyboxIndex].name;
+ }
+ return "None";
+ }
+
+ ///
+ /// Gets the total number of available skyboxes.
+ ///
+ public int GetSkyboxCount()
+ {
+ return availableSkyboxes != null ? availableSkyboxes.Length : 0;
+ }
+
+ ///
+ /// Returns the current skybox index.
+ ///
+ public int GetCurrentSkyboxIndex()
+ {
+ return currentSkyboxIndex;
+ }
+
+ ///
+ /// Sets the skybox rotation (useful for animated or time-based rotations).
+ ///
+ public void SetSkyboxRotation(float rotationAmount)
+ {
+ if (RenderSettings.skybox != null)
+ {
+ RenderSettings.skybox.SetFloat("_Rotation", rotationAmount);
+ }
+ }
+
+ // Call from UI button OnClick event:
+ public void OnCycleSkyboxButtonClicked()
+ {
+ GetComponent().NextSkybox();
+ }
+}