From a35382fd466fef0b254d3812c69cc8b1bbd052fc Mon Sep 17 00:00:00 2001 From: Bloody Date: Fri, 10 Jul 2026 18:51:26 +0200 Subject: [PATCH 1/9] test: add Data Center tooling expectations --- tests/datacenter-tooling.tests.ps1 | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/datacenter-tooling.tests.ps1 diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 new file mode 100644 index 0000000..040c00e --- /dev/null +++ b/tests/datacenter-tooling.tests.ps1 @@ -0,0 +1,48 @@ +$ErrorActionPreference = "Stop" + +$RepoRoot = Split-Path -Parent $PSScriptRoot + +function Read-RepoFile { + param([string]$Path) + + Get-Content -Raw -LiteralPath (Join-Path $RepoRoot $Path) +} + +function Assert-Contains { + param( + [string]$Name, + [string]$Text, + [string]$Pattern + ) + + if ($Text -notmatch $Pattern) { + throw "FAIL: $Name did not contain pattern: $Pattern" + } + + Write-Host "PASS: $Name" +} + +$Installer = Read-RepoFile "install-datacenter.ps1" +$Workflow = Read-RepoFile ".github\workflows\release-datacenter-coreclr.yml" +$Readme = Read-RepoFile "README.md" +$Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" + +Assert-Contains "installer supports auto-detect helper" $Installer "Find-DataCenterGameDir" +Assert-Contains "installer supports status mode" $Installer "\[switch\]\$Status" +Assert-Contains "installer supports uninstall mode" $Installer "\[switch\]\$Uninstall" +Assert-Contains "installer supports restore backup mode" $Installer "\[string\]\$RestoreBackup" +Assert-Contains "installer reports install state" $Installer "Get-InstallState" +Assert-Contains "installer can open logs" $Installer "\[switch\]\$OpenLogs" + +Assert-Contains "release workflow checks existing tags" $Workflow "gh release view \$tag" +Assert-Contains "release workflow validates zip contents" $Workflow "Expand-Archive" +Assert-Contains "release workflow reads changelog" $Workflow "CHANGELOG.md" + +Assert-Contains "readme documents uninstall" $Readme "-Uninstall" +Assert-Contains "readme documents restore backup" $Readme "-RestoreBackup" +Assert-Contains "readme documents support package" $Readme "collect-datacenter-logs.ps1" + +Assert-Contains "troubleshooting documents status command" $Troubleshooting "-Status" +Assert-Contains "troubleshooting documents duplicated files" $Troubleshooting "doppelt|duplicate" + +Write-Host "All Data Center tooling tests passed." From eae7df8b103be5d4623d232cdacd372b4faa36a0 Mon Sep 17 00:00:00 2001 From: Bloody Date: Fri, 10 Jul 2026 19:11:49 +0200 Subject: [PATCH 2/9] feat: complete Data Center improvement package --- .../workflows/release-datacenter-coreclr.yml | 87 ++- CHANGELOG.md | 16 +- README.md | 20 + docs/CHANGELOG_DATACENTER_DE_EN.md | 10 + docs/DATACENTER_KNOWN_ISSUES.md | 40 ++ docs/INSTALL_DATACENTER_DE_EN.md | 18 + docs/TROUBLESHOOTING_DATACENTER_DE.md | 34 + docs/testing/datacenter-improvements.tdd.md | 56 ++ install-datacenter.ps1 | 599 +++++++++++++++--- src/Config/ConfigManager.cs | 25 + src/ExplorerCore.cs | 9 +- src/Hooks/HookCreator.cs | 2 +- src/ObjectExplorer/ObjectSearch.cs | 185 +++++- src/ObjectExplorer/SearchProvider.cs | 109 +++- src/Runtime/DataCenterCompatibility.cs | 42 ++ src/Runtime/DataCenterDiagnostics.cs | 134 ++++ src/UI/Panels/CSConsolePanel.cs | 3 +- src/UI/Panels/ObjectExplorerPanel.cs | 3 +- src/UI/Panels/OptionsPanel.cs | 74 +++ src/UI/UIManager.cs | 9 +- .../GenericConstructorWidget.cs | 3 +- tests/datacenter-tooling.tests.ps1 | 10 +- update-datacenter-known-issues.ps1 | 88 +++ 23 files changed, 1433 insertions(+), 143 deletions(-) create mode 100644 docs/DATACENTER_KNOWN_ISSUES.md create mode 100644 docs/testing/datacenter-improvements.tdd.md create mode 100644 src/Runtime/DataCenterCompatibility.cs create mode 100644 src/Runtime/DataCenterDiagnostics.cs create mode 100644 update-datacenter-known-issues.ps1 diff --git a/.github/workflows/release-datacenter-coreclr.yml b/.github/workflows/release-datacenter-coreclr.yml index ddc4706..9773782 100644 --- a/.github/workflows/release-datacenter-coreclr.yml +++ b/.github/workflows/release-datacenter-coreclr.yml @@ -1,6 +1,9 @@ name: Release Data Center CoreCLR on: + push: + tags: + - "v*" workflow_dispatch: inputs: tag: @@ -25,6 +28,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Guard release tag + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: | + $eventName = '${{ github.event_name }}' + $tag = if ($eventName -eq 'push') { '${{ github.ref_name }}' } else { '${{ inputs.tag }}' } + if ([string]::IsNullOrWhiteSpace($tag)) { + throw "Release tag is required." + } + + gh release view $tag *> $null + if ($LASTEXITCODE -eq 0) { + throw "Release $tag already exists." + } + + if ($eventName -ne 'push') { + git rev-parse "refs/tags/$tag" *> $null + if ($LASTEXITCODE -eq 0) { + throw "Tag $tag already exists." + } + } + - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -36,20 +62,61 @@ jobs: shell: pwsh run: .\build-datacenter.ps1 + - name: Validate release package + shell: pwsh + run: | + $zip = 'Release\UnityExplorer.MelonLoader.IL2CPP.CoreCLR.zip' + $checksums = 'Release\CHECKSUMS_SHA256.txt' + if (!(Test-Path -LiteralPath $zip)) { + throw "Release ZIP missing: $zip" + } + if (!(Test-Path -LiteralPath $checksums)) { + throw "Checksum file missing: $checksums" + } + + $extractDir = Join-Path $env:RUNNER_TEMP 'datacenter-release-check' + Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $extractDir | Out-Null + Expand-Archive -LiteralPath $zip -DestinationPath $extractDir -Force + + $expectedFiles = @( + 'Mods\UnityExplorer.ML.IL2CPP.CoreCLR.dll', + 'UserLibs\UniverseLib.ML.IL2CPP.Interop.dll' + ) + + foreach ($relativePath in $expectedFiles) { + $fullPath = Join-Path $extractDir $relativePath + if (!(Test-Path -LiteralPath $fullPath)) { + throw "Release ZIP is missing $relativePath." + } + } + - name: Prepare release notes shell: pwsh run: | - $tag = '${{ inputs.tag }}' - $title = '${{ inputs.title }}' + $eventName = '${{ github.event_name }}' + $tag = if ($eventName -eq 'push') { '${{ github.ref_name }}' } else { '${{ inputs.tag }}' } + $title = if ($eventName -eq 'push') { "" } else { '${{ inputs.title }}' } if ([string]::IsNullOrWhiteSpace($title)) { $title = "UnityExplorer Data Center Safe Fork $tag" } + $changelog = Get-Content -Raw -LiteralPath 'CHANGELOG.md' + $unreleased = "" + $match = [regex]::Match($changelog, "(?ms)^## Unreleased\s*(?.*?)(?=^##\s|\z)") + if ($match.Success) { + $unreleased = $match.Groups['body'].Value.Trim() + } + $notes = @( "# $title", "", "Data Center CoreCLR build for Unity 6000.x / IL2CPP / MelonLoader Open-Beta.", "", + "## Changes", + "", + ($(if ([string]::IsNullOrWhiteSpace($unreleased)) { "See CHANGELOG.md for the current changes." } else { $unreleased })), + "", "## Install", "", "Copy the included folders into your Data Center game directory:", @@ -69,8 +136,9 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - $tag = '${{ inputs.tag }}' - $title = '${{ inputs.title }}' + $eventName = '${{ github.event_name }}' + $tag = if ($eventName -eq 'push') { '${{ github.ref_name }}' } else { '${{ inputs.tag }}' } + $title = if ($eventName -eq 'push') { "" } else { '${{ inputs.title }}' } if ([string]::IsNullOrWhiteSpace($title)) { $title = "UnityExplorer Data Center Safe Fork $tag" } @@ -80,11 +148,16 @@ jobs: 'Release\UnityExplorer.MelonLoader.IL2CPP.CoreCLR.zip', 'Release\CHECKSUMS_SHA256.txt', '--title', $title, - '--notes-file', 'release-notes.md', - '--target', '${{ github.sha }}' + '--notes-file', 'release-notes.md' ) - if ('${{ inputs.prerelease }}' -eq 'true') { + if ($eventName -ne 'push') { + $args += @('--target', '${{ github.sha }}') + } + + $isPrerelease = if ($eventName -eq 'push') { $tag -match '-' } else { '${{ inputs.prerelease }}' -eq 'true' } + + if ($isPrerelease) { $args += '--prerelease' } else { diff --git a/CHANGELOG.md b/CHANGELOG.md index 034f8f5..2f05247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ All notable changes to this Data Center-focused UnityExplorer fork are documente ### English - Added a support package script for collecting Data Center logs, file hashes and detected runtime versions. -- Improved `install-datacenter.ps1` with dry-run, wait-for-exit, MelonLoader preflight and duplicate-file warnings. -- Added a manual GitHub Actions release workflow for Data Center CoreCLR builds. +- Improved `install-datacenter.ps1` with auto-detect, status, open-logs, uninstall, restore-backup, dry-run, wait-for-exit, MelonLoader preflight and duplicate-file warnings. +- Added in-game Data Center runtime status, log opening and support package buttons to the Options panel. +- Added configurable Unity 6000 safe-mode switches for scene, input, scroll and TimeScale fallbacks. +- Expanded Object Search with tag, layer, component and active-state filters plus recent/favorite working sets. +- Added a manual GitHub Actions release workflow with tag guards, ZIP validation and changelog-derived release notes. +- Added a generated local Data Center known-issues/support-matrix document. - Centralized Unity 6000 safe UI fallback helpers. - Added clearer runtime compatibility logging. - GitHub Actions now uploads the ready-to-use CoreCLR ZIP plus generated SHA256 checksums. @@ -19,8 +23,12 @@ All notable changes to this Data Center-focused UnityExplorer fork are documente ### Deutsch - Support-Paket-Skript zum Sammeln von Data-Center-Logs, Datei-Hashes und erkannten Runtime-Versionen hinzugefügt. -- `install-datacenter.ps1` mit Dry-Run, Warten auf Spielende, MelonLoader-Vorprüfung und Warnungen vor doppelten Dateien verbessert. -- Manuellen GitHub-Actions-Release-Workflow für Data-Center-CoreCLR-Builds hinzugefügt. +- `install-datacenter.ps1` mit Auto-Erkennung, Status, Logs-öffnen, Uninstall, Backup-Restore, Dry-Run, Warten auf Spielende, MelonLoader-Vorprüfung und Warnungen vor doppelten Dateien verbessert. +- Ingame Data-Center-Runtime-Status, Logs-öffnen und Support-Paket-Buttons im Options-Panel ergänzt. +- Konfigurierbare Unity-6000-Safe-Mode-Schalter für Scene-, Input-, Scroll- und TimeScale-Fallbacks hinzugefügt. +- Object Search um Tag-, Layer-, Komponenten- und Aktivitätsfilter sowie Recent/Favorites-Arbeitslisten erweitert. +- Manuellen GitHub-Actions-Release-Workflow mit Tag-Prüfung, ZIP-Validierung und Changelog-basierten Release Notes hinzugefügt. +- Generierte lokale Data-Center-Known-Issues-/Support-Matrix ergänzt. - Unity-6000-Safe-UI-Fallback-Helfer zentralisiert. - Klareres Runtime-Kompatibilitätslogging ergänzt. - GitHub Actions lädt jetzt die fertige CoreCLR-ZIP plus erzeugte SHA256-Prüfsummen hoch. diff --git a/README.md b/README.md index 0b8886c..37742b4 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,14 @@ Nützliche Installationsoptionen: ```powershell powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Status +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -OpenLogs +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Uninstall +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -RestoreBackup latest ``` +Ohne `-GameDir` versucht das Skript, den Data-Center-Ordner automatisch über Steam zu finden. + Ausführliche Anleitung: [Installationsanleitung](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation) @@ -146,8 +152,11 @@ Support-Paket für Fehlersuche erstellen: ```powershell powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1 +powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 ``` +Im Spiel enthält das Options-Panel zusätzlich einen **Data Center Runtime Status** mit Buttons zum Aktualisieren, Logs öffnen und Support-ZIP erstellen. + --- ## Aus dem Source Code bauen @@ -189,6 +198,7 @@ Weitere Informationen findest du im Wiki: - [Lokale Installation / Local Installation](docs/INSTALL_DATACENTER_DE_EN.md) - [Lokales Troubleshooting / Local Troubleshooting](docs/TROUBLESHOOTING_DATACENTER_DE.md) +- [Lokale Known Issues / Local Known Issues](docs/DATACENTER_KNOWN_ISSUES.md) - [Home](https://github.com/BloddyMichi/Unity-Explorer-/wiki) - [Installation](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation) - [Known Issues](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Known-Issues) @@ -368,8 +378,14 @@ Useful install options: ```powershell powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Status +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -OpenLogs +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Uninstall +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -RestoreBackup latest ``` +When `-GameDir` is omitted, the script tries to auto-detect the Data Center folder from Steam. + Detailed instructions: [Installation Guide](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation) @@ -403,8 +419,11 @@ Create a support package for troubleshooting: ```powershell powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1 +powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 ``` +In game, the Options panel also includes a **Data Center Runtime Status** block with buttons to refresh status, open logs and create a support ZIP. + --- ## Build From Source @@ -457,6 +476,7 @@ The wiki contains additional setup, troubleshooting and development information: - [Local Installation](docs/INSTALL_DATACENTER_DE_EN.md) - [Local Troubleshooting](docs/TROUBLESHOOTING_DATACENTER_DE.md) +- [Local Known Issues](docs/DATACENTER_KNOWN_ISSUES.md) - [Home](https://github.com/BloddyMichi/Unity-Explorer-/wiki) - [Installation](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation) - [Known Issues](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Known-Issues) diff --git a/docs/CHANGELOG_DATACENTER_DE_EN.md b/docs/CHANGELOG_DATACENTER_DE_EN.md index 950e59c..45ddc81 100644 --- a/docs/CHANGELOG_DATACENTER_DE_EN.md +++ b/docs/CHANGELOG_DATACENTER_DE_EN.md @@ -36,6 +36,11 @@ - Support-Paket-Skript für Logs, Hashes und Runtime-Versionen ergänzt. - Install-Skript mit Dry-Run, Warten auf Spielende und Vorabprüfungen verbessert. - Manuellen GitHub-Actions-Release-Workflow ergänzt. +- Install-Skript um Auto-Erkennung, Status, Logs-öffnen, Uninstall und Backup-Restore erweitert. +- Options-Panel um Data-Center-Runtime-Status und Ingame-Support-Paket erweitert. +- Unity-6000-Safe-Mode-Schalter konfigurierbar gemacht. +- Object Search um Tag-, Layer-, Komponenten- und Aktivitätsfilter sowie Recent/Favorites erweitert. +- Lokalen Known-Issues-/Support-Matrix-Generator ergänzt. ## English @@ -73,3 +78,8 @@ - Added a support package script for logs, hashes and runtime versions. - Improved the install script with dry-run, wait-for-exit and preflight checks. - Added a manual GitHub Actions release workflow. +- Extended the install script with auto-detect, status, open-logs, uninstall and backup restore. +- Added Data Center runtime status and in-game support package creation to the Options panel. +- Made Unity 6000 safe-mode fallbacks configurable. +- Expanded Object Search with tag, layer, component and active-state filters plus recent/favorites. +- Added a local known-issues/support-matrix generator. diff --git a/docs/DATACENTER_KNOWN_ISSUES.md b/docs/DATACENTER_KNOWN_ISSUES.md new file mode 100644 index 0000000..eb852af --- /dev/null +++ b/docs/DATACENTER_KNOWN_ISSUES.md @@ -0,0 +1,40 @@ +# Data Center Known Issues + +Generated: 2026-07-10 19:08:54 +02:00 + +## Detected Runtime + +- [18:38:23.835] Unity Version: 6000.4.12f1 +- [18:38:23.562] MelonLoader v0.7.3 Open-Beta +- [18:38:23.835] Game Version: UNKNOWN +- Game directory: C:\Program Files (x86)\Steam\steamapps\common\Data Center + +## Support Matrix + +| Area | Status | Notes | +|---|---|---| +| Unity 6000.x / IL2CPP / CoreCLR startup | Supported | Tested with Unity 6000.4.12f1 and MelonLoader 0.7.3 Open-Beta. | +| C# Console | Supported with fallback | Uses safe multi-line input on Unity 6000. | +| Hook Manager | Supported with fallback | Uses safe editor input and generic argument container on Unity 6000. | +| Object Search | Supported | Preferred replacement for Scene Explorer in Data Center. | +| Scene Explorer | Disabled by default | Disabled because SceneManager scene access can throw AccessViolationException on Unity 6000 IL2CPP/CoreCLR. | +| TimeScale widget | Disabled by default | Can be re-enabled from options if needed, but restart is recommended. | +| UniverseLib UI AssetBundle | Disabled by default | Built-in UI resources are used on Unity 6000. | + +## Known Issues + +- MelonLoader can log an early `EndOfStreamException` while reading game info. If UnityExplorer initializes afterwards, this is not currently blocking. +- Some Unity UIElements generic type cache warnings can appear during UniverseLib startup. They are logged as warnings and have not blocked the Data Center flow. +- Layer name lookup is avoided in Unity 6000 safe paths; numeric layer labels may appear instead. +- If UnityExplorer does not load, run `.\install-datacenter.ps1 -Status` and check for duplicate or misplaced DLLs. + +## Useful Commands + +```powershell +.\install-datacenter.ps1 -Status +.\install-datacenter.ps1 -BuildIfMissing -DryRun +.\install-datacenter.ps1 -Uninstall +.\install-datacenter.ps1 -RestoreBackup latest +.\collect-datacenter-logs.ps1 +.\update-datacenter-known-issues.ps1 +``` diff --git a/docs/INSTALL_DATACENTER_DE_EN.md b/docs/INSTALL_DATACENTER_DE_EN.md index 68366a7..f87fcb0 100644 --- a/docs/INSTALL_DATACENTER_DE_EN.md +++ b/docs/INSTALL_DATACENTER_DE_EN.md @@ -45,9 +45,15 @@ Nützliche Optionen: ```powershell powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Status +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -OpenLogs +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Uninstall +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -RestoreBackup latest ``` `-DryRun` zeigt die geplanten Dateien und SHA256-Hashes, ohne etwas zu kopieren. `-WaitForGameExit` wartet, bis Data Center beendet wurde, bevor Dateien ersetzt werden. +`-Status` zeigt installierte Hashes, Backups, laufende Prozesse und doppelte Dateien. `-Uninstall` entfernt die installierten Dateien mit Backup. `-RestoreBackup latest` stellt das neueste Backup wieder her. +Ohne `-GameDir` versucht das Skript, den Data-Center-Ordner automatisch über Steam zu finden. Bei einem anderen Installationspfad: @@ -75,8 +81,11 @@ Bei Problemen kannst du ein Support-Paket erzeugen: ```powershell powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1 +powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 ``` +Im Spiel kann das Options-Panel ebenfalls ein Support-ZIP erstellen und Logs öffnen. + ## English ### Recommended path @@ -122,9 +131,15 @@ Useful options: ```powershell powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Status +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -OpenLogs +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -Uninstall +powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -RestoreBackup latest ``` `-DryRun` prints the planned files and SHA256 hashes without copying anything. `-WaitForGameExit` waits until Data Center has exited before replacing files. +`-Status` prints installed hashes, backups, running processes and duplicate files. `-Uninstall` removes the installed files after backing them up. `-RestoreBackup latest` restores the newest backup. +When `-GameDir` is omitted, the script tries to auto-detect the Data Center folder from Steam. For a custom install path: @@ -152,4 +167,7 @@ If something fails, create a support package: ```powershell powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1 +powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 ``` + +The in-game Options panel can also create a support ZIP and open logs. diff --git a/docs/TROUBLESHOOTING_DATACENTER_DE.md b/docs/TROUBLESHOOTING_DATACENTER_DE.md index 6435622..0129f3f 100644 --- a/docs/TROUBLESHOOTING_DATACENTER_DE.md +++ b/docs/TROUBLESHOOTING_DATACENTER_DE.md @@ -80,6 +80,23 @@ Data Center\UserLibs\UniverseLib.ML.IL2CPP.Interop.dll - Wenn du das Install-Skript verwendest, prüfe den letzten Ordner unter `Data Center\UnityExplorer.Backups`. - Nutze `install-datacenter.ps1 -DryRun`, um vorab Zielpfade und Hashes zu prüfen. - Nutze `install-datacenter.ps1 -WaitForGameExit`, wenn Data Center noch läuft. +- Nutze `install-datacenter.ps1 -Status`, um installierte Hashes, laufende Prozesse, Backups und doppelte Dateien zu prüfen. +- Nutze `install-datacenter.ps1 -Uninstall`, um UnityExplorer mit Backup zu entfernen. +- Nutze `install-datacenter.ps1 -RestoreBackup latest`, um das neueste Backup wiederherzustellen. +- Nutze `install-datacenter.ps1 -OpenLogs`, um `Latest.log` und den UnityExplorer-Logordner zu öffnen. + +**Doppelte oder falsch platzierte Dateien** + +Diese Dateien können verhindern, dass die richtige Version geladen wird: + +```text +Data Center\Mods\UniverseLib.ML.IL2CPP.Interop.dll +Data Center\Mods\UniverseLib.ML.IL2CPP.dll +Data Center\Mods\UnityExplorer.ML.IL2CPP.dll +Data Center\UserLibs\UnityExplorer.ML.IL2CPP.CoreCLR.dll +``` + +`install-datacenter.ps1 -Status` warnt vor diesen Duplikaten. ## English @@ -161,3 +178,20 @@ Data Center\UserLibs\UniverseLib.ML.IL2CPP.Interop.dll - If you use the install script, check the latest folder under `Data Center\UnityExplorer.Backups`. - Use `install-datacenter.ps1 -DryRun` to check target paths and hashes before copying. - Use `install-datacenter.ps1 -WaitForGameExit` if Data Center is still running. +- Use `install-datacenter.ps1 -Status` to check installed hashes, running processes, backups and duplicate files. +- Use `install-datacenter.ps1 -Uninstall` to remove UnityExplorer after creating a backup. +- Use `install-datacenter.ps1 -RestoreBackup latest` to restore the newest backup. +- Use `install-datacenter.ps1 -OpenLogs` to open `Latest.log` and the UnityExplorer log folder. + +**Duplicate or misplaced files** + +These files can stop the correct version from loading: + +```text +Data Center\Mods\UniverseLib.ML.IL2CPP.Interop.dll +Data Center\Mods\UniverseLib.ML.IL2CPP.dll +Data Center\Mods\UnityExplorer.ML.IL2CPP.dll +Data Center\UserLibs\UnityExplorer.ML.IL2CPP.CoreCLR.dll +``` + +`install-datacenter.ps1 -Status` warns about these duplicate files. diff --git a/docs/testing/datacenter-improvements.tdd.md b/docs/testing/datacenter-improvements.tdd.md new file mode 100644 index 0000000..bedff9e --- /dev/null +++ b/docs/testing/datacenter-improvements.tdd.md @@ -0,0 +1,56 @@ +# Data Center Improvements TDD Evidence + +## Source + +Journeys were derived from the requested 1-to-6 improvement package: + +- Installer comfort features. +- In-game diagnostics. +- Release automation. +- Configurable Unity 6000 safe mode. +- Expanded Object Search. +- Generated known-issues documentation. + +## RED Evidence + +Command: + +```powershell +powershell -ExecutionPolicy Bypass -File .\tests\datacenter-tooling.tests.ps1 +``` + +Initial result: + +```text +FAIL: installer supports auto-detect helper did not contain pattern: Find-DataCenterGameDir +``` + +This confirmed that the expected installer/status/release-documentation capabilities did not exist before implementation. + +## GREEN Evidence + +Command: + +```powershell +powershell -ExecutionPolicy Bypass -File .\tests\datacenter-tooling.tests.ps1 +``` + +Final result: + +```text +All Data Center tooling tests passed. +``` + +## Test Specification + +| # | Guarantee | Test file or command | Type | Result | +|---|---|---|---|---| +| 1 | Installer exposes auto-detect, status, uninstall, restore-backup and open-log modes. | `tests/datacenter-tooling.tests.ps1` | script contract | PASS | +| 2 | Release workflow guards existing releases/tags, validates ZIP contents and reads `CHANGELOG.md`. | `tests/datacenter-tooling.tests.ps1` | workflow contract | PASS | +| 3 | README documents uninstall, restore-backup and support-package commands. | `tests/datacenter-tooling.tests.ps1` | docs contract | PASS | +| 4 | Troubleshooting docs mention status checks and duplicate file handling. | `tests/datacenter-tooling.tests.ps1` | docs contract | PASS | +| 5 | Data Center CoreCLR build compiles after runtime changes. | `powershell -ExecutionPolicy Bypass -File .\build-datacenter.ps1` | build | PASS | + +## Coverage And Gaps + +This repository does not currently include a C# unit-test project or an automated Unity runtime harness. The PowerShell contract tests cover the script and release workflow surface. The C# Unity UI changes are covered by the Data Center CoreCLR build and require manual in-game verification after installing the produced DLLs. diff --git a/install-datacenter.ps1 b/install-datacenter.ps1 index 28f41fd..b529589 100644 --- a/install-datacenter.ps1 +++ b/install-datacenter.ps1 @@ -1,85 +1,166 @@ param( - [string]$GameDir = "C:\Program Files (x86)\Steam\steamapps\common\Data Center", + [string]$GameDir, [switch]$BuildIfMissing, [switch]$SkipBackup, [switch]$SkipProcessCheck, [switch]$WaitForGameExit, - [switch]$DryRun + [switch]$DryRun, + [switch]$Status, + [switch]$Uninstall, + [string]$RestoreBackup, + [switch]$OpenLogs ) $ErrorActionPreference = "Stop" $BuildScript = Join-Path $PSScriptRoot "build-datacenter.ps1" $BuildDir = Join-Path $PSScriptRoot "Release\UnityExplorer.MelonLoader.IL2CPP.CoreCLR" -$ModDll = Join-Path $BuildDir "Mods\UnityExplorer.ML.IL2CPP.CoreCLR.dll" -$UserLib = Join-Path $BuildDir "UserLibs\UniverseLib.ML.IL2CPP.Interop.dll" +$ModDllName = "UnityExplorer.ML.IL2CPP.CoreCLR.dll" +$UserLibName = "UniverseLib.ML.IL2CPP.Interop.dll" +$ModDll = Join-Path $BuildDir "Mods\$ModDllName" +$UserLib = Join-Path $BuildDir "UserLibs\$UserLibName" -if (!(Test-Path $ModDll) -or !(Test-Path $UserLib)) { - if ($BuildIfMissing) { - Write-Host "Build output not found. Running build-datacenter.ps1..." -ForegroundColor Yellow - & powershell -ExecutionPolicy Bypass -File $BuildScript - if ($LASTEXITCODE -ne 0) { - throw "build-datacenter.ps1 failed with exit code $LASTEXITCODE." - } +function Add-UniquePath { + param( + [System.Collections.Generic.List[string]]$Paths, + [string]$Path + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + return } - else { - throw "Build output not found. Run .\build-datacenter.ps1 first, or use -BuildIfMissing." + + $Normalized = $Path.Trim().Trim('"') -replace '/', '\' + if (!$Paths.Contains($Normalized)) { + $Paths.Add($Normalized) } } -if (!(Test-Path $ModDll) -or !(Test-Path $UserLib)) { - throw "Build output is still missing after build step." +function Get-SteamInstallRoots { + $Paths = [System.Collections.Generic.List[string]]::new() + + Add-UniquePath $Paths "C:\Program Files (x86)\Steam" + Add-UniquePath $Paths "C:\Program Files\Steam" + + foreach ($RegistryPath in @( + "HKCU:\Software\Valve\Steam", + "HKLM:\SOFTWARE\Valve\Steam", + "HKLM:\SOFTWARE\Wow6432Node\Valve\Steam" + )) { + try { + $SteamKey = Get-ItemProperty -LiteralPath $RegistryPath -ErrorAction Stop + Add-UniquePath $Paths $SteamKey.SteamPath + Add-UniquePath $Paths $SteamKey.InstallPath + } + catch { + } + } + + return $Paths } -$ResolvedGameDir = Resolve-Path -LiteralPath $GameDir -ErrorAction SilentlyContinue -if ($ResolvedGameDir) { - $GameDir = $ResolvedGameDir.Path +function Get-SteamLibraryRoots { + $Libraries = [System.Collections.Generic.List[string]]::new() + + foreach ($SteamRoot in Get-SteamInstallRoots) { + Add-UniquePath $Libraries $SteamRoot + + $LibraryFile = Join-Path $SteamRoot "steamapps\libraryfolders.vdf" + if (!(Test-Path -LiteralPath $LibraryFile)) { + continue + } + + foreach ($Line in Get-Content -LiteralPath $LibraryFile) { + if ($Line -match '"path"\s+"(?.+)"') { + Add-UniquePath $Libraries ($Matches.path -replace '\\\\', '\') + } + } + } + + foreach ($Drive in Get-PSDrive -PSProvider FileSystem) { + Add-UniquePath $Libraries (Join-Path $Drive.Root "SteamLibrary") + Add-UniquePath $Libraries (Join-Path $Drive.Root "Steam") + } + + return $Libraries } -else { - throw "Game directory not found: $GameDir. Pass -GameDir with the correct Data Center install path." + +function Find-DataCenterGameDir { + $Candidates = [System.Collections.Generic.List[string]]::new() + Add-UniquePath $Candidates "C:\Program Files (x86)\Steam\steamapps\common\Data Center" + + foreach ($LibraryRoot in Get-SteamLibraryRoots) { + Add-UniquePath $Candidates (Join-Path $LibraryRoot "steamapps\common\Data Center") + } + + foreach ($Candidate in $Candidates) { + if (Test-Path -LiteralPath $Candidate) { + return (Resolve-Path -LiteralPath $Candidate).Path + } + } + + return $null } -$ModsDir = Join-Path $GameDir "Mods" -$UserLibsDir = Join-Path $GameDir "UserLibs" -$BackupDir = Join-Path $GameDir ("UnityExplorer.Backups\" + (Get-Date -Format "yyyyMMdd-HHmmss")) -$DidBackup = $false +function Resolve-DataCenterGameDir { + param([string]$RequestedGameDir) -function Backup-ExistingFile { - param([string]$Path) + if (![string]::IsNullOrWhiteSpace($RequestedGameDir)) { + $Resolved = Resolve-Path -LiteralPath $RequestedGameDir -ErrorAction SilentlyContinue + if ($Resolved) { + return $Resolved.Path + } - if ($SkipBackup -or !(Test-Path $Path)) { - return + throw "Game directory not found: $RequestedGameDir. Pass -GameDir with the correct Data Center install path." } - if (!$script:DidBackup) { - New-Item -ItemType Directory -Force -Path $script:BackupDir | Out-Null - $script:DidBackup = $true + $Detected = Find-DataCenterGameDir + if ($Detected) { + Write-Host "Auto-detected Data Center directory:" -ForegroundColor Cyan + Write-Host $Detected + return $Detected } - Copy-Item -LiteralPath $Path -Destination (Join-Path $script:BackupDir (Split-Path $Path -Leaf)) -Force + throw "Could not auto-detect Data Center. Pass -GameDir with the correct install path." } -function Write-InstalledFile { - param([string]$Path) - - $Hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() - Write-Host " $Path" - Write-Host " SHA256 $Hash" +function Get-InstalledPaths { + param([string]$ResolvedGameDir) + + $ModsDir = Join-Path $ResolvedGameDir "Mods" + $UserLibsDir = Join-Path $ResolvedGameDir "UserLibs" + + [pscustomobject]@{ + ModsDir = $ModsDir + UserLibsDir = $UserLibsDir + ModDll = Join-Path $ModsDir $ModDllName + UserLib = Join-Path $UserLibsDir $UserLibName + BackupRoot = Join-Path $ResolvedGameDir "UnityExplorer.Backups" + MelonLoaderDir = Join-Path $ResolvedGameDir "MelonLoader" + MelonLoaderDll = Join-Path $ResolvedGameDir "MelonLoader\net6\MelonLoader.dll" + MelonLog = Join-Path $ResolvedGameDir "MelonLoader\Latest.log" + UnityExplorerLogDir = Join-Path $ResolvedGameDir "Mods\sinai-dev-UnityExplorer\Logs" + } } -function Write-PlannedFile { - param( - [string]$Source, - [string]$Destination - ) +function Get-HashOrStatus { + param([string]$Path) - $Hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Source).Hash.ToLowerInvariant() - Write-Host " $Source" - Write-Host " -> $Destination" - Write-Host " SHA256 $Hash" + if (!(Test-Path -LiteralPath $Path)) { + return "missing" + } + + try { + return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() + } + catch { + return "locked_or_unreadable" + } } function Get-DataCenterProcesses { + param([string]$ResolvedGameDir) + $Processes = @() foreach ($Process in Get-Process) { @@ -91,7 +172,7 @@ function Get-DataCenterProcesses { $ProcessPath = $null } - if ($ProcessPath -and $ProcessPath.StartsWith($GameDir, [System.StringComparison]::OrdinalIgnoreCase)) { + if ($ProcessPath -and $ProcessPath.StartsWith($ResolvedGameDir, [System.StringComparison]::OrdinalIgnoreCase)) { $Processes += $Process } } @@ -99,6 +180,149 @@ function Get-DataCenterProcesses { return $Processes } +function Get-DuplicateFiles { + param([pscustomobject]$Paths) + + @( + (Join-Path $Paths.ModsDir "UniverseLib.ML.IL2CPP.Interop.dll"), + (Join-Path $Paths.ModsDir "UniverseLib.ML.IL2CPP.dll"), + (Join-Path $Paths.ModsDir "UnityExplorer.ML.IL2CPP.dll"), + (Join-Path $Paths.UserLibsDir "UnityExplorer.ML.IL2CPP.CoreCLR.dll") + ) | Where-Object { Test-Path -LiteralPath $_ } +} + +function Get-LatestBackup { + param([pscustomobject]$Paths) + + if (!(Test-Path -LiteralPath $Paths.BackupRoot)) { + return $null + } + + Get-ChildItem -LiteralPath $Paths.BackupRoot -Directory | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 +} + +function Get-InstallState { + param( + [string]$ResolvedGameDir, + [pscustomobject]$Paths + ) + + $BuildModHash = if (Test-Path -LiteralPath $ModDll) { Get-HashOrStatus $ModDll } else { "build_missing" } + $BuildUserLibHash = if (Test-Path -LiteralPath $UserLib) { Get-HashOrStatus $UserLib } else { "build_missing" } + $InstalledModHash = Get-HashOrStatus $Paths.ModDll + $InstalledUserLibHash = Get-HashOrStatus $Paths.UserLib + $RunningProcesses = @(Get-DataCenterProcesses $ResolvedGameDir) + $Duplicates = @(Get-DuplicateFiles $Paths) + $LatestBackup = Get-LatestBackup $Paths + + $ModState = if ($InstalledModHash -eq "missing") { + "missing" + } + elseif ($BuildModHash -ne "build_missing" -and $InstalledModHash -eq $BuildModHash) { + "current" + } + else { + "installed_different_or_older" + } + + $UserLibState = if ($InstalledUserLibHash -eq "missing") { + "missing" + } + elseif ($BuildUserLibHash -ne "build_missing" -and $InstalledUserLibHash -eq $BuildUserLibHash) { + "current" + } + else { + "installed_different_or_older" + } + + [pscustomobject]@{ + GameDir = $ResolvedGameDir + ModState = $ModState + UserLibState = $UserLibState + InstalledModHash = $InstalledModHash + InstalledUserLibHash = $InstalledUserLibHash + BuildModHash = $BuildModHash + BuildUserLibHash = $BuildUserLibHash + MelonLoader = if (Test-Path -LiteralPath $Paths.MelonLoaderDir) { "found" } else { "missing" } + MelonLoaderNet6 = if (Test-Path -LiteralPath $Paths.MelonLoaderDll) { "found" } else { "missing" } + RunningProcesses = $RunningProcesses + DuplicateFiles = $Duplicates + LatestBackup = $LatestBackup + LatestLog = if (Test-Path -LiteralPath $Paths.MelonLog) { $Paths.MelonLog } else { $null } + UnityExplorerLogDir = if (Test-Path -LiteralPath $Paths.UnityExplorerLogDir) { $Paths.UnityExplorerLogDir } else { $null } + } +} + +function Write-InstallState { + param([pscustomobject]$State) + + Write-Host "Data Center UnityExplorer install state" -ForegroundColor Cyan + Write-Host "GameDir: $($State.GameDir)" + Write-Host "MelonLoader: $($State.MelonLoader) (net6 dll: $($State.MelonLoaderNet6))" + Write-Host "Mod DLL: $($State.ModState)" + Write-Host " installed: $($State.InstalledModHash)" + Write-Host " build: $($State.BuildModHash)" + Write-Host "UserLib DLL: $($State.UserLibState)" + Write-Host " installed: $($State.InstalledUserLibHash)" + Write-Host " build: $($State.BuildUserLibHash)" + + if ($State.RunningProcesses.Count -gt 0) { + Write-Host "Running Data Center processes:" -ForegroundColor Yellow + foreach ($Process in $State.RunningProcesses) { + Write-Host (" {0} (PID {1})" -f $Process.ProcessName, $Process.Id) + } + } + else { + Write-Host "Running Data Center processes: none" + } + + if ($State.DuplicateFiles.Count -gt 0) { + Write-Host "Potential old or misplaced duplicate files:" -ForegroundColor Yellow + foreach ($Duplicate in $State.DuplicateFiles) { + Write-Host " $Duplicate" + } + } + else { + Write-Host "Potential old or misplaced duplicate files: none" + } + + if ($State.LatestBackup) { + Write-Host "Latest backup: $($State.LatestBackup.FullName)" + } + else { + Write-Host "Latest backup: none" + } + + if ($State.LatestLog) { + Write-Host "Latest MelonLoader log: $($State.LatestLog)" + } + + if ($State.UnityExplorerLogDir) { + Write-Host "UnityExplorer logs: $($State.UnityExplorerLogDir)" + } +} + +function Ensure-BuildOutput { + if (!(Test-Path -LiteralPath $ModDll) -or !(Test-Path -LiteralPath $UserLib)) { + if ($BuildIfMissing) { + Write-Host "Build output not found. Running build-datacenter.ps1..." -ForegroundColor Yellow + & powershell -ExecutionPolicy Bypass -File $BuildScript + if ($LASTEXITCODE -ne 0) { + throw "build-datacenter.ps1 failed with exit code $LASTEXITCODE." + } + } + else { + throw "Build output not found. Run .\build-datacenter.ps1 first, or use -BuildIfMissing." + } + } + + if (!(Test-Path -LiteralPath $ModDll) -or !(Test-Path -LiteralPath $UserLib)) { + throw "Build output is still missing after build step." + } +} + function Assert-FileIsReplaceable { param([string]$Path) @@ -121,104 +345,269 @@ function Assert-FileIsReplaceable { } function Assert-MelonLoaderInstall { - $MelonLoaderDir = Join-Path $GameDir "MelonLoader" - if (!(Test-Path -LiteralPath $MelonLoaderDir)) { - throw "MelonLoader was not found in this game directory: $MelonLoaderDir`nInstall MelonLoader for Data Center first, then rerun this script." + param([pscustomobject]$Paths) + + if (!(Test-Path -LiteralPath $Paths.MelonLoaderDir)) { + throw "MelonLoader was not found in this game directory: $($Paths.MelonLoaderDir)`nInstall MelonLoader for Data Center first, then rerun this script." } - $MelonLoaderDll = Join-Path $MelonLoaderDir "net6\MelonLoader.dll" - if (!(Test-Path -LiteralPath $MelonLoaderDll)) { + if (!(Test-Path -LiteralPath $Paths.MelonLoaderDll)) { Write-Host "MelonLoader folder found, but net6\MelonLoader.dll was not detected. This may be a different or incomplete MelonLoader install." -ForegroundColor Yellow } } -function Write-DuplicateWarnings { - $KnownDuplicateFiles = @( - (Join-Path $ModsDir "UniverseLib.ML.IL2CPP.Interop.dll"), - (Join-Path $ModsDir "UniverseLib.ML.IL2CPP.dll"), - (Join-Path $ModsDir "UnityExplorer.ML.IL2CPP.dll"), - (Join-Path $UserLibsDir "UnityExplorer.ML.IL2CPP.CoreCLR.dll") - ) +function Wait-Or-FailIfGameIsRunning { + param([string]$ResolvedGameDir) - $FoundAny = $false - foreach ($Path in $KnownDuplicateFiles) { - if (Test-Path -LiteralPath $Path) { - if (!$FoundAny) { - Write-Host "Potential old or misplaced UnityExplorer files found:" -ForegroundColor Yellow - $FoundAny = $true + if ($SkipProcessCheck) { + return + } + + $RunningGameProcesses = @(Get-DataCenterProcesses $ResolvedGameDir) + if ($RunningGameProcesses.Count -eq 0) { + return + } + + if ($WaitForGameExit) { + Write-Host "Waiting for Data Center to exit before continuing..." -ForegroundColor Yellow + while ($RunningGameProcesses.Count -gt 0) { + foreach ($Process in $RunningGameProcesses) { + Write-Host (" still running: {0} (PID {1})" -f $Process.ProcessName, $Process.Id) } - Write-Host " $Path" + Start-Sleep -Seconds 2 + $RunningGameProcesses = @(Get-DataCenterProcesses $ResolvedGameDir) } + + return } - if ($FoundAny) { - Write-Host "Consider removing these files if UnityExplorer does not load correctly." -ForegroundColor Yellow + Write-Host "Data Center appears to be running from this game directory:" -ForegroundColor Yellow + foreach ($Process in $RunningGameProcesses) { + Write-Host (" {0} (PID {1})" -f $Process.ProcessName, $Process.Id) } + + throw "Close Data Center before changing UnityExplorer files, or rerun with -WaitForGameExit." } -$InstalledModDll = Join-Path $ModsDir "UnityExplorer.ML.IL2CPP.CoreCLR.dll" -$InstalledUserLib = Join-Path $UserLibsDir "UniverseLib.ML.IL2CPP.Interop.dll" +function Backup-ExistingFile { + param( + [string]$Path, + [string]$BackupDir + ) -if (!$SkipProcessCheck) { - $RunningGameProcesses = Get-DataCenterProcesses - if ($RunningGameProcesses.Count -gt 0) { - if ($WaitForGameExit) { - Write-Host "Waiting for Data Center to exit before installing..." -ForegroundColor Yellow - while ($RunningGameProcesses.Count -gt 0) { - foreach ($Process in $RunningGameProcesses) { - Write-Host (" still running: {0} (PID {1})" -f $Process.ProcessName, $Process.Id) - } + if ($SkipBackup -or !(Test-Path -LiteralPath $Path)) { + return $false + } - Start-Sleep -Seconds 2 - $RunningGameProcesses = Get-DataCenterProcesses - } - } - else { - Write-Host "Data Center appears to be running from this game directory:" -ForegroundColor Yellow - foreach ($Process in $RunningGameProcesses) { - Write-Host (" {0} (PID {1})" -f $Process.ProcessName, $Process.Id) - } + New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null + Copy-Item -LiteralPath $Path -Destination (Join-Path $BackupDir (Split-Path $Path -Leaf)) -Force + return $true +} + +function Write-PlannedFile { + param( + [string]$Source, + [string]$Destination + ) + + $Hash = (Get-HashOrStatus $Source) + Write-Host " $Source" + Write-Host " -> $Destination" + Write-Host " SHA256 $Hash" +} + +function Write-InstalledFile { + param([string]$Path) + + $Hash = Get-HashOrStatus $Path + Write-Host " $Path" + Write-Host " SHA256 $Hash" +} + +function Write-DuplicateWarnings { + param([pscustomobject]$Paths) + + $DuplicateFiles = @(Get-DuplicateFiles $Paths) + if ($DuplicateFiles.Count -eq 0) { + return + } - throw "Close Data Center before installing UnityExplorer, or rerun with -WaitForGameExit." + Write-Host "Potential old or misplaced UnityExplorer duplicate files found:" -ForegroundColor Yellow + foreach ($Path in $DuplicateFiles) { + Write-Host " $Path" + } + + Write-Host "Consider removing these files if UnityExplorer does not load correctly." -ForegroundColor Yellow +} + +function Invoke-OpenLogs { + param([pscustomobject]$Paths) + + if (Test-Path -LiteralPath $Paths.MelonLog) { + Invoke-Item -LiteralPath $Paths.MelonLog + Write-Host "Opened MelonLoader log: $($Paths.MelonLog)" -ForegroundColor Green + } + else { + Write-Host "MelonLoader Latest.log not found: $($Paths.MelonLog)" -ForegroundColor Yellow + } + + if (Test-Path -LiteralPath $Paths.UnityExplorerLogDir) { + Invoke-Item -LiteralPath $Paths.UnityExplorerLogDir + Write-Host "Opened UnityExplorer log folder: $($Paths.UnityExplorerLogDir)" -ForegroundColor Green + } + else { + Write-Host "UnityExplorer log folder not found: $($Paths.UnityExplorerLogDir)" -ForegroundColor Yellow + } +} + +function Resolve-BackupToRestore { + param( + [pscustomobject]$Paths, + [string]$RequestedBackup + ) + + if ([string]::IsNullOrWhiteSpace($RequestedBackup) -or $RequestedBackup.Equals("latest", [System.StringComparison]::OrdinalIgnoreCase)) { + $LatestBackup = Get-LatestBackup $Paths + if (!$LatestBackup) { + throw "No UnityExplorer backup folder was found under: $($Paths.BackupRoot)" } + + return $LatestBackup.FullName + } + + if (Test-Path -LiteralPath $RequestedBackup) { + return (Resolve-Path -LiteralPath $RequestedBackup).Path + } + + $NamedBackup = Join-Path $Paths.BackupRoot $RequestedBackup + if (Test-Path -LiteralPath $NamedBackup) { + return (Resolve-Path -LiteralPath $NamedBackup).Path + } + + throw "Backup not found: $RequestedBackup" +} + +$GameDir = Resolve-DataCenterGameDir $GameDir +$Paths = Get-InstalledPaths $GameDir +$BackupDir = Join-Path $Paths.BackupRoot (Get-Date -Format "yyyyMMdd-HHmmss") +$NeedsBuildOutput = !$Status -and !$Uninstall -and [string]::IsNullOrWhiteSpace($RestoreBackup) -and !$OpenLogs + +if ($OpenLogs) { + Invoke-OpenLogs $Paths + if (!$Status -and !$Uninstall -and [string]::IsNullOrWhiteSpace($RestoreBackup) -and !$DryRun) { + exit 0 + } +} + +if ($Status) { + Write-InstallState (Get-InstallState $GameDir $Paths) + if (!$Uninstall -and [string]::IsNullOrWhiteSpace($RestoreBackup) -and !$DryRun) { + exit 0 + } +} + +if ($NeedsBuildOutput -or $DryRun) { + Ensure-BuildOutput +} + +Wait-Or-FailIfGameIsRunning $GameDir + +if ($Uninstall) { + Assert-FileIsReplaceable $Paths.ModDll + Assert-FileIsReplaceable $Paths.UserLib + + if ($DryRun) { + Write-Host "Dry run only. No files were removed." -ForegroundColor Cyan + Write-Host "Would remove:" + Write-Host " $($Paths.ModDll)" + Write-Host " $($Paths.UserLib)" + exit 0 + } + + $DidBackup = $false + $DidBackup = (Backup-ExistingFile $Paths.ModDll $BackupDir) -or $DidBackup + $DidBackup = (Backup-ExistingFile $Paths.UserLib $BackupDir) -or $DidBackup + + Remove-Item -LiteralPath $Paths.ModDll -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $Paths.UserLib -ErrorAction SilentlyContinue + + Write-Host "Removed Data Center UnityExplorer Safe Fork files." -ForegroundColor Green + if ($DidBackup) { + Write-Host "Removed files were backed up to:" -ForegroundColor Yellow + Write-Host $BackupDir } + + exit 0 } -Assert-MelonLoaderInstall +if (![string]::IsNullOrWhiteSpace($RestoreBackup)) { + $BackupToRestore = Resolve-BackupToRestore $Paths $RestoreBackup + $BackupModDll = Join-Path $BackupToRestore $ModDllName + $BackupUserLib = Join-Path $BackupToRestore $UserLibName + + if (!(Test-Path -LiteralPath $BackupModDll) -or !(Test-Path -LiteralPath $BackupUserLib)) { + throw "Backup is incomplete. Expected $ModDllName and $UserLibName in: $BackupToRestore" + } + + Assert-FileIsReplaceable $Paths.ModDll + Assert-FileIsReplaceable $Paths.UserLib + + if ($DryRun) { + Write-Host "Dry run only. No backup files were restored." -ForegroundColor Cyan + Write-Host "Would restore:" + Write-Host " $BackupModDll -> $($Paths.ModDll)" + Write-Host " $BackupUserLib -> $($Paths.UserLib)" + exit 0 + } + + New-Item -ItemType Directory -Force -Path $Paths.ModsDir | Out-Null + New-Item -ItemType Directory -Force -Path $Paths.UserLibsDir | Out-Null + Copy-Item -LiteralPath $BackupModDll -Destination $Paths.ModDll -Force + Copy-Item -LiteralPath $BackupUserLib -Destination $Paths.UserLib -Force + + Write-Host "Restored UnityExplorer backup:" -ForegroundColor Green + Write-Host $BackupToRestore + Write-Host "Restored files:" + Write-InstalledFile $Paths.ModDll + Write-InstalledFile $Paths.UserLib + exit 0 +} -New-Item -ItemType Directory -Force -Path $ModsDir | Out-Null -New-Item -ItemType Directory -Force -Path $UserLibsDir | Out-Null +Assert-MelonLoaderInstall $Paths -Write-DuplicateWarnings +New-Item -ItemType Directory -Force -Path $Paths.ModsDir | Out-Null +New-Item -ItemType Directory -Force -Path $Paths.UserLibsDir | Out-Null -Assert-FileIsReplaceable $InstalledModDll -Assert-FileIsReplaceable $InstalledUserLib +Write-DuplicateWarnings $Paths +Assert-FileIsReplaceable $Paths.ModDll +Assert-FileIsReplaceable $Paths.UserLib if ($DryRun) { Write-Host "Dry run only. No files were copied." -ForegroundColor Cyan Write-Host "Planned install target:" -ForegroundColor Cyan Write-Host $GameDir Write-Host "Planned files:" -ForegroundColor Cyan - Write-PlannedFile $ModDll $InstalledModDll - Write-PlannedFile $UserLib $InstalledUserLib + Write-PlannedFile $ModDll $Paths.ModDll + Write-PlannedFile $UserLib $Paths.UserLib exit 0 } -Backup-ExistingFile $InstalledModDll -Backup-ExistingFile $InstalledUserLib +$DidInstallBackup = $false +$DidInstallBackup = (Backup-ExistingFile $Paths.ModDll $BackupDir) -or $DidInstallBackup +$DidInstallBackup = (Backup-ExistingFile $Paths.UserLib $BackupDir) -or $DidInstallBackup -Copy-Item -LiteralPath $ModDll -Destination $InstalledModDll -Force -Copy-Item -LiteralPath $UserLib -Destination $InstalledUserLib -Force +Copy-Item -LiteralPath $ModDll -Destination $Paths.ModDll -Force +Copy-Item -LiteralPath $UserLib -Destination $Paths.UserLib -Force Write-Host "Installed Data Center UnityExplorer Safe Fork to:" -ForegroundColor Green Write-Host $GameDir -if ($DidBackup) { +if ($DidInstallBackup) { Write-Host "Previous files were backed up to:" -ForegroundColor Yellow Write-Host $BackupDir } Write-Host "Installed files:" -ForegroundColor Green -Write-InstalledFile $InstalledModDll -Write-InstalledFile $InstalledUserLib +Write-InstalledFile $Paths.ModDll +Write-InstalledFile $Paths.UserLib diff --git a/src/Config/ConfigManager.cs b/src/Config/ConfigManager.cs index 3d36ab6..37ded8f 100644 --- a/src/Config/ConfigManager.cs +++ b/src/Config/ConfigManager.cs @@ -36,6 +36,11 @@ public static class ConfigManager public static ConfigElement Menu_Language; public static ConfigElement World_MouseInspect_Keybind; public static ConfigElement UI_MouseInspect_Keybind; + public static ConfigElement Unity6000_EnableSafeMode; + public static ConfigElement Unity6000_DisableSceneExplorer; + public static ConfigElement Unity6000_UseSafeInputFallbacks; + public static ConfigElement Unity6000_UseSafeScrollFallbacks; + public static ConfigElement Unity6000_DisableTimeScaleWidget; public static ConfigElement CSConsole_Assembly_Blacklist; public static ConfigElement Reflection_Signature_Blacklist; @@ -162,6 +167,26 @@ private static void CreateConfigElements() "Optional keybind to begin a UI-mode Mouse Inspect.", KeyCode.None); + Unity6000_EnableSafeMode = new("Unity 6000 Safe Mode", + "Enable Data Center compatibility fallbacks for Unity 6000 / IL2CPP / CoreCLR. Restart recommended when changing this setting.", + true); + + Unity6000_DisableSceneExplorer = new("Unity 6000 Disable Scene Explorer", + "Disable Scene Explorer and SceneManager scene scans on Unity 6000. Object Search remains available. Restart required.", + true); + + Unity6000_UseSafeInputFallbacks = new("Unity 6000 Safe Input Fallbacks", + "Use simpler multi-line input fields for the C# Console and Hook Editor on Unity 6000. Restart required.", + true); + + Unity6000_UseSafeScrollFallbacks = new("Unity 6000 Safe Scroll Fallbacks", + "Replace scroll-view based UI paths which are unstable in Data Center on Unity 6000. Restart required.", + true); + + Unity6000_DisableTimeScaleWidget = new("Unity 6000 Disable TimeScale Widget", + "Hide the TimeScale widget on Unity 6000 to avoid early UI initialization instability. Restart required.", + true); + CSConsole_Assembly_Blacklist = new("CSharp Console Assembly Blacklist", "Use this to blacklist Assembly names from being referenced by the C# Console. Requires a Reset of the C# Console.\n" + "Separate each Assembly with a semicolon ';'." + diff --git a/src/ExplorerCore.cs b/src/ExplorerCore.cs index aa9e41e..c1f19df 100644 --- a/src/ExplorerCore.cs +++ b/src/ExplorerCore.cs @@ -44,11 +44,11 @@ public static void Init(IExplorerLoader loader) Loader = loader; Log($"{NAME} {VERSION} initializing..."); - LogRuntimeCompatibility(); CheckLegacyExplorerFolder(); Directory.CreateDirectory(ExplorerFolder); ConfigManager.Init(Loader.ConfigHandler); + LogRuntimeCompatibility(); Universe.Init(ConfigManager.Startup_Delay_Time.Value, LateInit, Log, new() { @@ -69,7 +69,7 @@ public static void Init(IExplorerLoader loader) // Default delay is 1 second which is usually enough. static void LateInit() { - if (IsUnity6000OrNewer) + if (DataCenterCompatibility.DisableSceneExplorer) { Log($"Skipping SceneHandler.Init on Unity {Application.unityVersion} due to Unity 6 IL2CPP/CoreCLR stability issues."); } @@ -102,11 +102,12 @@ static void LateInit() private static void LogRuntimeCompatibility() { string unityVersion = Application.unityVersion ?? "unknown"; - string safeMode = IsUnity6000OrNewer ? "enabled" : "disabled"; + string safeMode = DataCenterCompatibility.SafeModeActive ? "enabled" : "disabled"; Log($"Runtime compatibility: Unity {unityVersion}, Unity 6000 safe mode {safeMode}."); + Log(DataCenterCompatibility.GetSummary()); - if (IsUnity6000OrNewer) + if (DataCenterCompatibility.SafeModeActive) { Log("Unity 6000 safe mode will disable or replace unstable scene, scroll-view and input-field paths."); } diff --git a/src/Hooks/HookCreator.cs b/src/Hooks/HookCreator.cs index 0f39353..684779f 100644 --- a/src/Hooks/HookCreator.cs +++ b/src/Hooks/HookCreator.cs @@ -301,7 +301,7 @@ public void ConstructEditor(GameObject parent) editorDoneButton.OnClick += EditorInputCancel; int fontSize = 16; - if (ExplorerCore.IsUnity6000OrNewer) + if (DataCenterCompatibility.UseSafeInputFallbacks) { ConstructSafeEditorInput(fontSize); } diff --git a/src/ObjectExplorer/ObjectSearch.cs b/src/ObjectExplorer/ObjectSearch.cs index 4417464..954a730 100644 --- a/src/ObjectExplorer/ObjectSearch.cs +++ b/src/ObjectExplorer/ObjectSearch.cs @@ -19,6 +19,7 @@ public ObjectSearch(ObjectExplorerPanel parent) private SearchContext context = SearchContext.UnityObject; private SceneFilter sceneFilter = SceneFilter.Any; private ChildFilter childFilter = ChildFilter.Any; + private ActiveFilter activeFilter = ActiveFilter.Any; private string desiredTypeInput; private string lastCheckedTypeInput; private bool lastTypeCanHaveGameObject; @@ -26,6 +27,9 @@ public ObjectSearch(ObjectExplorerPanel parent) public ButtonListHandler dataHandler; private ScrollPool resultsScrollPool; private List currentResults = new(); + private readonly List recentObjects = new(); + private readonly List favoriteObjects = new(); + private string currentResultMode = "search"; //public TypeCompleter typeAutocompleter; public TypeCompleter unityObjectTypeCompleter; @@ -37,7 +41,14 @@ public ObjectSearch(ObjectExplorerPanel parent) private GameObject childFilterRow; private GameObject classInputRow; private GameObject nameInputRow; + private GameObject tagInputRow; + private GameObject layerInputRow; + private GameObject componentInputRow; + private GameObject activeFilterRow; private InputFieldRef nameInputField; + private InputFieldRef tagInputField; + private InputFieldRef layerInputField; + private InputFieldRef componentInputField; private Text resultsLabel; public List GetEntries() => currentResults; @@ -47,16 +58,30 @@ public void DoSearch() cachedCellTexts.Clear(); if (context == SearchContext.Singleton) + { + currentResultMode = "singleton"; currentResults = SearchProvider.InstanceSearch(desiredTypeInput).ToList(); + } else if (context == SearchContext.Class) + { + currentResultMode = "class"; currentResults = SearchProvider.ClassSearch(desiredTypeInput); + } else - currentResults = SearchProvider.UnityObjectSearch(nameInputField.Text, desiredTypeInput, childFilter, sceneFilter); - - dataHandler.RefreshData(); - resultsScrollPool.Refresh(true); + { + currentResultMode = "search"; + currentResults = SearchProvider.UnityObjectSearch( + nameInputField.Text, + desiredTypeInput, + childFilter, + sceneFilter, + tagInputField.Text, + layerInputField.Text, + componentInputField.Text, + activeFilter); + } - resultsLabel.text = $"{currentResults.Count} results"; + RefreshResults(); } public void Update() @@ -92,7 +117,16 @@ private void OnContextDropdownChanged(int value) sceneFilterRow.SetActive(false); childFilterRow.SetActive(false); - nameInputRow.SetActive(context == SearchContext.UnityObject); + if (nameInputRow != null) + nameInputRow.SetActive(context == SearchContext.UnityObject); + if (tagInputRow != null) + tagInputRow.SetActive(context == SearchContext.UnityObject); + if (layerInputRow != null) + layerInputRow.SetActive(context == SearchContext.UnityObject); + if (componentInputRow != null) + componentInputRow.SetActive(context == SearchContext.UnityObject); + if (activeFilterRow != null) + activeFilterRow.SetActive(context == SearchContext.UnityObject); switch (context) { @@ -112,6 +146,8 @@ private void OnContextDropdownChanged(int value) private void OnChildFilterDropChanged(int value) => childFilter = (ChildFilter)value; + private void OnActiveFilterDropChanged(int value) => activeFilter = (ActiveFilter)value; + private void OnTypeInputChanged(string val) { desiredTypeInput = val; @@ -148,12 +184,79 @@ public void SetCell(ButtonCell cell, int index) private void OnCellClicked(int dataIndex) { + AddRecent(currentResults[dataIndex]); + if (context == SearchContext.Class) InspectorManager.Inspect(currentResults[dataIndex] as Type); else InspectorManager.Inspect(currentResults[dataIndex]); } + private void ShowFavorites() + { + currentResultMode = "favorites"; + currentResults = favoriteObjects.Where(IsStillValid).ToList(); + RefreshResults(); + } + + private void ShowRecent() + { + currentResultMode = "recent"; + currentResults = recentObjects.Where(IsStillValid).ToList(); + RefreshResults(); + } + + private void ClearFavorites() + { + favoriteObjects.Clear(); + if (currentResultMode == "favorites") + ShowFavorites(); + } + + private void AddActiveInspectorToFavorites() + { + object target = InspectorManager.ActiveInspector?.Target; + if (!IsStillValid(target)) + { + ExplorerCore.LogWarning("No active inspector target is available to favorite."); + return; + } + + AddUnique(favoriteObjects, target, 50); + ExplorerCore.Log("Added favorite Object Search target: " + ToStringUtility.ToStringWithType(target, target.GetActualType())); + ShowFavorites(); + } + + private void AddRecent(object target) + { + AddUnique(recentObjects, target, 25); + } + + private static void AddUnique(List list, object target, int maxCount) + { + if (!IsStillValid(target)) + return; + + list.RemoveAll(item => item.ReferenceEqual(target)); + list.Insert(0, target); + + while (list.Count > maxCount) + list.RemoveAt(list.Count - 1); + } + + private static bool IsStillValid(object target) + { + return target != null && !target.IsNullOrDestroyed(); + } + + private void RefreshResults() + { + cachedCellTexts.Clear(); + dataHandler.RefreshData(); + resultsScrollPool.Refresh(true); + resultsLabel.text = $"{currentResults.Count} {currentResultMode} result(s) | recent {recentObjects.Count} | favorites {favoriteObjects.Count}"; + } + private bool ShouldDisplayCell(object arg1, string arg2) => true; public override void ConstructUI(GameObject parent) @@ -237,12 +340,78 @@ public override void ConstructUI(GameObject parent) nameInputField = UIFactory.CreateInputField(nameInputRow, "NameFilterInput", "..."); UIFactory.SetLayoutElement(nameInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + // Tag filter input + + tagInputRow = UIFactory.CreateHorizontalGroup(uiRoot, "TagRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(tagInputRow, minHeight: 25, flexibleHeight: 0); + + Text tagLbl = UIFactory.CreateLabel(tagInputRow, "TagFilterLabel", "Tag contains:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(tagLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + tagInputField = UIFactory.CreateInputField(tagInputRow, "TagFilterInput", "..."); + UIFactory.SetLayoutElement(tagInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + // Layer filter input + + layerInputRow = UIFactory.CreateHorizontalGroup(uiRoot, "LayerRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(layerInputRow, minHeight: 25, flexibleHeight: 0); + + Text layerLbl = UIFactory.CreateLabel(layerInputRow, "LayerFilterLabel", "Layer contains:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(layerLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + layerInputField = UIFactory.CreateInputField(layerInputRow, "LayerFilterInput", "number or name"); + UIFactory.SetLayoutElement(layerInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + // Component filter input + + componentInputRow = UIFactory.CreateHorizontalGroup(uiRoot, "ComponentRow", true, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(componentInputRow, minHeight: 25, flexibleHeight: 0); + + Text componentLbl = UIFactory.CreateLabel(componentInputRow, "ComponentFilterLabel", "Has component:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(componentLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + componentInputField = UIFactory.CreateInputField(componentInputRow, "ComponentFilterInput", "..."); + UIFactory.SetLayoutElement(componentInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + _ = new TypeCompleter(typeof(Component), componentInputField, true, false, true); + + // Active filter row + + activeFilterRow = UIFactory.CreateHorizontalGroup(uiRoot, "ActiveFilterRow", false, true, true, true, 2, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(activeFilterRow, minHeight: 25, flexibleHeight: 0); + + Text activeLbl = UIFactory.CreateLabel(activeFilterRow, "ActiveLabel", "Active filter:", TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(activeLbl.gameObject, minWidth: 110, flexibleWidth: 0); + + GameObject activeDropObj = UIFactory.CreateDropdown(activeFilterRow, "ActiveFilterDropdown", out Dropdown activeDrop, null, 14, OnActiveFilterDropChanged); + foreach (string name in Enum.GetNames(typeof(ActiveFilter))) + activeDrop.options.Add(new Dropdown.OptionData(name)); + UIFactory.SetLayoutElement(activeDropObj, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + // Search button - ButtonRef searchButton = UIFactory.CreateButton(uiRoot, "SearchButton", "Search"); - UIFactory.SetLayoutElement(searchButton.Component.gameObject, minHeight: 25, flexibleHeight: 0); + GameObject actionRow = UIFactory.CreateHorizontalGroup(uiRoot, "SearchActionRow", false, false, true, true, 4, new Vector4(2, 2, 2, 2)); + UIFactory.SetLayoutElement(actionRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + + ButtonRef searchButton = UIFactory.CreateButton(actionRow, "SearchButton", "Search"); + UIFactory.SetLayoutElement(searchButton.Component.gameObject, minHeight: 25, minWidth: 90, flexibleHeight: 0); searchButton.OnClick += DoSearch; + ButtonRef recentButton = UIFactory.CreateButton(actionRow, "RecentButton", "Recent"); + UIFactory.SetLayoutElement(recentButton.Component.gameObject, minHeight: 25, minWidth: 90, flexibleHeight: 0); + recentButton.OnClick += ShowRecent; + + ButtonRef favoritesButton = UIFactory.CreateButton(actionRow, "FavoritesButton", "Favorites"); + UIFactory.SetLayoutElement(favoritesButton.Component.gameObject, minHeight: 25, minWidth: 100, flexibleHeight: 0); + favoritesButton.OnClick += ShowFavorites; + + ButtonRef favoriteActiveButton = UIFactory.CreateButton(actionRow, "FavoriteActiveButton", "Favorite Active"); + UIFactory.SetLayoutElement(favoriteActiveButton.Component.gameObject, minHeight: 25, minWidth: 130, flexibleHeight: 0); + favoriteActiveButton.OnClick += AddActiveInspectorToFavorites; + + ButtonRef clearFavoritesButton = UIFactory.CreateButton(actionRow, "ClearFavoritesButton", "Clear"); + UIFactory.SetLayoutElement(clearFavoritesButton.Component.gameObject, minHeight: 25, minWidth: 80, flexibleHeight: 0); + clearFavoritesButton.OnClick += ClearFavorites; + // Results count label GameObject resultsCountRow = UIFactory.CreateHorizontalGroup(uiRoot, "ResultsCountRow", true, true, true, true); diff --git a/src/ObjectExplorer/SearchProvider.cs b/src/ObjectExplorer/SearchProvider.cs index 2f7c424..fd60dfd 100644 --- a/src/ObjectExplorer/SearchProvider.cs +++ b/src/ObjectExplorer/SearchProvider.cs @@ -24,6 +24,13 @@ public enum SceneFilter HideAndDontSave, } +public enum ActiveFilter +{ + Any, + ActiveInHierarchy, + InactiveInHierarchy +} + public static class SearchProvider { private static bool Filter(Scene scene, SceneFilter filter) @@ -38,7 +45,15 @@ private static bool Filter(Scene scene, SceneFilter filter) }; } - internal static List UnityObjectSearch(string input, string customTypeInput, ChildFilter childFilter, SceneFilter sceneFilter) + internal static List UnityObjectSearch( + string input, + string customTypeInput, + ChildFilter childFilter, + SceneFilter sceneFilter, + string tagInput = null, + string layerInput = null, + string componentTypeInput = null, + ActiveFilter activeFilter = ActiveFilter.Any) { List results = new(); @@ -66,6 +81,7 @@ internal static List UnityObjectSearch(string input, string customTypeIn nameFilter = input; bool shouldFilterGOs = searchType == typeof(GameObject) || typeof(Component).IsAssignableFrom(searchType); + Type componentFilterType = ResolveComponentFilter(componentTypeInput); foreach (UnityEngine.Object obj in allObjects) { @@ -85,6 +101,9 @@ internal static List UnityObjectSearch(string input, string customTypeIn if (go.transform.root.name == "UniverseLibCanvas") continue; + if (!MatchesGameObjectFilters(go, tagInput, layerInput, componentFilterType, activeFilter)) + continue; + if (shouldFilterGOs) { if (sceneFilter != SceneFilter.Any) @@ -112,6 +131,92 @@ internal static List UnityObjectSearch(string input, string customTypeIn return results; } + private static Type ResolveComponentFilter(string componentTypeInput) + { + if (string.IsNullOrWhiteSpace(componentTypeInput)) + return null; + + Type componentType = ReflectionUtility.GetTypeByName(componentTypeInput); + if (componentType == null) + { + ExplorerCore.LogWarning($"Could not find component type by name '{componentTypeInput}'!"); + return null; + } + + if (!typeof(Component).IsAssignableFrom(componentType)) + { + ExplorerCore.LogWarning($"Component filter '{componentType.FullName}' is not assignable from UnityEngine.Component!"); + return null; + } + + return componentType; + } + + private static bool MatchesGameObjectFilters( + GameObject go, + string tagInput, + string layerInput, + Type componentFilterType, + ActiveFilter activeFilter) + { + if (!string.IsNullOrWhiteSpace(tagInput)) + { + string tag = string.Empty; + try { tag = go.tag ?? string.Empty; } + catch { tag = string.Empty; } + + if (!tag.ContainsIgnoreCase(tagInput)) + return false; + } + + if (!string.IsNullOrWhiteSpace(layerInput)) + { + string layerNumber = go.layer.ToString(); + string layerName = string.Empty; + + if (!ExplorerCore.IsUnity6000OrNewer) + { + try { layerName = LayerMask.LayerToName(go.layer) ?? string.Empty; } + catch { layerName = string.Empty; } + } + + if (!layerNumber.ContainsIgnoreCase(layerInput) && !layerName.ContainsIgnoreCase(layerInput)) + return false; + } + + if (activeFilter == ActiveFilter.ActiveInHierarchy && !go.activeInHierarchy) + return false; + + if (activeFilter == ActiveFilter.InactiveInHierarchy && go.activeInHierarchy) + return false; + + if (componentFilterType != null) + { + try + { +#if CPP +#if INTEROP + if (!go.GetComponent(Il2CppInterop.Runtime.Il2CppType.From(componentFilterType))) + return false; +#else + if (!go.GetComponent(UnhollowerRuntimeLib.Il2CppType.From(componentFilterType))) + return false; +#endif +#else + if (!go.GetComponent(componentFilterType)) + return false; +#endif + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Component filter failed for '" + go.name + "': " + ex.Message); + return false; + } + } + + return true; + } + internal static List ClassSearch(string input) { List list = new(); @@ -181,4 +286,4 @@ internal static List InstanceSearch(string input) return instances.Distinct().ToList(); } -} \ No newline at end of file +} diff --git a/src/Runtime/DataCenterCompatibility.cs b/src/Runtime/DataCenterCompatibility.cs new file mode 100644 index 0000000..4c43681 --- /dev/null +++ b/src/Runtime/DataCenterCompatibility.cs @@ -0,0 +1,42 @@ +using UnityExplorer.Config; + +namespace UnityExplorer.Runtime; + +internal static class DataCenterCompatibility +{ + public static bool SafeModeActive => + ExplorerCore.IsUnity6000OrNewer && + (ConfigManager.Unity6000_EnableSafeMode?.Value ?? true); + + public static bool DeferPanelBootstrap => + SafeModeActive; + + public static bool DisableSceneExplorer => + SafeModeActive && + (ConfigManager.Unity6000_DisableSceneExplorer?.Value ?? true); + + public static bool UseSafeInputFallbacks => + SafeModeActive && + (ConfigManager.Unity6000_UseSafeInputFallbacks?.Value ?? true); + + public static bool UseSafeScrollFallbacks => + SafeModeActive && + (ConfigManager.Unity6000_UseSafeScrollFallbacks?.Value ?? true); + + public static bool DisableTimeScaleWidget => + SafeModeActive && + (ConfigManager.Unity6000_DisableTimeScaleWidget?.Value ?? true); + + public static string GetSummary() + { + if (!ExplorerCore.IsUnity6000OrNewer) + return "Unity 6000 safe mode inactive: current Unity version does not require it."; + + return "Unity 6000 safe mode " + (SafeModeActive ? "enabled" : "disabled") + + $"; SceneExplorer disabled={DisableSceneExplorer}" + + $"; SafeInput={UseSafeInputFallbacks}" + + $"; SafeScroll={UseSafeScrollFallbacks}" + + $"; TimeScale disabled={DisableTimeScaleWidget}" + + $"; Deferred panels={DeferPanelBootstrap}"; + } +} diff --git a/src/Runtime/DataCenterDiagnostics.cs b/src/Runtime/DataCenterDiagnostics.cs new file mode 100644 index 0000000..9938e23 --- /dev/null +++ b/src/Runtime/DataCenterDiagnostics.cs @@ -0,0 +1,134 @@ +#if NET6_0_OR_GREATER +using System.IO.Compression; +#endif + +using UnityExplorer.Runtime; + +namespace UnityExplorer; + +internal static class DataCenterDiagnostics +{ + public static string GameDirectory + { + get + { + string modsDirectory = ExplorerCore.Loader?.ExplorerFolderDestination; + if (string.IsNullOrEmpty(modsDirectory)) + return ""; + + DirectoryInfo parent = Directory.GetParent(modsDirectory); + return parent?.FullName ?? ""; + } + } + + public static string SupportPackageDirectory => + Path.Combine(ExplorerCore.ExplorerFolder, "SupportPackages"); + + public static string GetStatusText() + { + string assemblyPath = typeof(ExplorerCore).Assembly.Location; + string loader = ExplorerCore.Loader?.GetType().Name ?? ""; + + return + "Data Center runtime status\n" + + $"Unity: {Application.unityVersion}\n" + + $"UnityExplorer: {ExplorerCore.VERSION}\n" + + $"Loader: {loader}\n" + + $"{DataCenterCompatibility.GetSummary()}\n" + + $"GameDir: {GameDirectory}\n" + + $"ExplorerFolder: {ExplorerCore.ExplorerFolder}\n" + + $"Assembly: {assemblyPath}"; + } + + public static string CreateSupportPackage() + { +#if NET6_0_OR_GREATER + Directory.CreateDirectory(SupportPackageDirectory); + + string timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + string stagingRoot = Path.Combine(SupportPackageDirectory, "DataCenter-UnityExplorer-Ingame-" + timestamp); + string zipPath = stagingRoot + ".zip"; + + if (Directory.Exists(stagingRoot)) + Directory.Delete(stagingRoot, true); + + Directory.CreateDirectory(stagingRoot); + + string gameDir = GameDirectory; + CopyIfExists(Path.Combine(gameDir, "MelonLoader", "Latest.log"), Path.Combine(stagingRoot, "MelonLoader")); + + string logDir = Path.Combine(ExplorerCore.ExplorerFolder, "Logs"); + if (Directory.Exists(logDir)) + { + string targetLogDir = Path.Combine(stagingRoot, "UnityExplorerLogs"); + Directory.CreateDirectory(targetLogDir); + + foreach (FileInfo log in new DirectoryInfo(logDir) + .GetFiles("*.txt") + .OrderByDescending(it => it.LastWriteTimeUtc) + .Take(10)) + { + File.Copy(log.FullName, Path.Combine(targetLogDir, log.Name), true); + } + } + + File.WriteAllText(Path.Combine(stagingRoot, "support-info.txt"), GetStatusText()); + + if (File.Exists(zipPath)) + File.Delete(zipPath); + + ZipFile.CreateFromDirectory(stagingRoot, zipPath); + Directory.Delete(stagingRoot, true); + + ExplorerCore.Log("Created Data Center support package: " + zipPath); + return zipPath; +#else + string message = "Ingame support package creation requires the net6/CoreCLR build."; + ExplorerCore.LogWarning(message); + return message; +#endif + } + + public static void OpenLogs() + { + OpenPath(Path.Combine(GameDirectory, "MelonLoader", "Latest.log")); + OpenPath(Path.Combine(ExplorerCore.ExplorerFolder, "Logs")); + } + + public static void OpenSupportPackages() + { + Directory.CreateDirectory(SupportPackageDirectory); + OpenPath(SupportPackageDirectory); + } + + private static void CopyIfExists(string source, string destinationDir) + { + if (!File.Exists(source)) + return; + + Directory.CreateDirectory(destinationDir); + File.Copy(source, Path.Combine(destinationDir, Path.GetFileName(source)), true); + } + + private static void OpenPath(string path) + { + try + { + if (!File.Exists(path) && !Directory.Exists(path)) + { + ExplorerCore.LogWarning("Path not found: " + path); + return; + } + + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = path, + UseShellExecute = true + }); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Failed to open path '" + path + "': " + ex.Message); + } + } +} diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs index b85646e..4bdec1c 100644 --- a/src/UI/Panels/CSConsolePanel.cs +++ b/src/UI/Panels/CSConsolePanel.cs @@ -1,5 +1,6 @@ using System.Collections; using UnityExplorer.CSConsole; +using UnityExplorer.Runtime; using UniverseLib.UI; using UniverseLib.UI.Models; using UniverseLib.UI.Widgets; @@ -141,7 +142,7 @@ protected override void ConstructPanelContent() int fontSize = 16; - if (ExplorerCore.IsUnity6000OrNewer) + if (DataCenterCompatibility.UseSafeInputFallbacks) { ConstructSafeInputField(inputArea, fontSize); } diff --git a/src/UI/Panels/ObjectExplorerPanel.cs b/src/UI/Panels/ObjectExplorerPanel.cs index f8abf11..0ae0465 100644 --- a/src/UI/Panels/ObjectExplorerPanel.cs +++ b/src/UI/Panels/ObjectExplorerPanel.cs @@ -1,4 +1,5 @@ using UnityExplorer.ObjectExplorer; +using UnityExplorer.Runtime; using UniverseLib.UI; using UniverseLib.UI.Models; @@ -25,7 +26,7 @@ public class ObjectExplorerPanel : UEPanel private readonly List tabButtons = new(); private static bool Unity6000SafeObjectExplorerMode => - Application.unityVersion != null && Application.unityVersion.StartsWith("6000"); + DataCenterCompatibility.DisableSceneExplorer; public ObjectExplorerPanel(UIBase owner) : base(owner) { diff --git a/src/UI/Panels/OptionsPanel.cs b/src/UI/Panels/OptionsPanel.cs index 3c4e9dc..76999d6 100644 --- a/src/UI/Panels/OptionsPanel.cs +++ b/src/UI/Panels/OptionsPanel.cs @@ -2,6 +2,7 @@ using UnityExplorer.CacheObject.Views; using UnityExplorer.Config; using UniverseLib.UI; +using UniverseLib.UI.Models; using UniverseLib.UI.Widgets.ScrollView; namespace UnityExplorer.UI.Panels @@ -95,6 +96,7 @@ protected override void ConstructPanelContent() UIFactory.SetLayoutElement(saveBtn.Component.gameObject, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 0); saveBtn.OnClick += ConfigManager.Handler.SaveConfig; + CreateDataCenterStatusBlock(); CreateMouseInspectKeybindDropdowns(); // Config entries @@ -108,6 +110,78 @@ protected override void ConstructPanelContent() scrollPool.Initialize(this); } + private void CreateDataCenterStatusBlock() + { + GameObject block = UIFactory.CreateVerticalGroup( + ContentRoot, + "DataCenterStatusBlock", + true, + false, + true, + true, + 4, + new Vector4(6, 6, 6, 6), + new Color(0.08f, 0.08f, 0.08f)); + + UIFactory.SetLayoutElement(block, minHeight: 175, flexibleHeight: 0, flexibleWidth: 9999); + + Text header = UIFactory.CreateLabel( + block, + "DataCenterStatusHeader", + "Data Center Runtime Status", + TextAnchor.MiddleLeft); + + UIFactory.SetLayoutElement(header.gameObject, minHeight: 22, flexibleWidth: 9999, flexibleHeight: 0); + + Text statusText = UIFactory.CreateLabel( + block, + "DataCenterStatusText", + DataCenterDiagnostics.GetStatusText(), + TextAnchor.UpperLeft); + + statusText.fontSize = 12; + statusText.horizontalOverflow = HorizontalWrapMode.Wrap; + statusText.verticalOverflow = VerticalWrapMode.Overflow; + UIFactory.SetLayoutElement(statusText.gameObject, minHeight: 92, flexibleWidth: 9999, flexibleHeight: 0); + + GameObject buttonRow = UIFactory.CreateHorizontalGroup( + block, + "DataCenterStatusButtonRow", + false, + false, + true, + true, + 4, + new Vector4(0, 0, 0, 0)); + + UIFactory.SetLayoutElement(buttonRow, minHeight: 28, flexibleHeight: 0, flexibleWidth: 9999); + + ButtonRef refreshButton = UIFactory.CreateButton(buttonRow, "RefreshDataCenterStatus", "Refresh"); + UIFactory.SetLayoutElement(refreshButton.Component.gameObject, minWidth: 100, minHeight: 26, flexibleWidth: 0); + refreshButton.OnClick += () => + { + statusText.text = DataCenterDiagnostics.GetStatusText(); + Notification.ShowMessage("Data Center status refreshed"); + }; + + ButtonRef supportButton = UIFactory.CreateButton(buttonRow, "CreateDataCenterSupportPackage", "Support ZIP"); + UIFactory.SetLayoutElement(supportButton.Component.gameObject, minWidth: 120, minHeight: 26, flexibleWidth: 0); + supportButton.OnClick += () => + { + string path = DataCenterDiagnostics.CreateSupportPackage(); + Notification.ShowMessage("Support package created"); + ExplorerCore.Log("Support package path: " + path); + }; + + ButtonRef logsButton = UIFactory.CreateButton(buttonRow, "OpenDataCenterLogs", "Open Logs"); + UIFactory.SetLayoutElement(logsButton.Component.gameObject, minWidth: 110, minHeight: 26, flexibleWidth: 0); + logsButton.OnClick += DataCenterDiagnostics.OpenLogs; + + ButtonRef packagesButton = UIFactory.CreateButton(buttonRow, "OpenDataCenterSupportPackages", "Packages"); + UIFactory.SetLayoutElement(packagesButton.Component.gameObject, minWidth: 110, minHeight: 26, flexibleWidth: 0); + packagesButton.OnClick += DataCenterDiagnostics.OpenSupportPackages; + } + private void CreateMouseInspectKeybindDropdowns() { GameObject block = UIFactory.CreateVerticalGroup( diff --git a/src/UI/UIManager.cs b/src/UI/UIManager.cs index 2021d67..801a8fd 100644 --- a/src/UI/UIManager.cs +++ b/src/UI/UIManager.cs @@ -1,6 +1,7 @@ using UnityExplorer.Config; using UnityExplorer.CSConsole; using UnityExplorer.Inspectors; +using UnityExplorer.Runtime; using UnityExplorer.UI.Panels; using UnityExplorer.UI.Widgets; using UniverseLib.Input; @@ -56,7 +57,7 @@ public enum VerticalAnchor private static bool mouseInspectorInitialized; private static bool consoleControllerInitialized; - private static bool UseUnity6000Fallbacks => ExplorerCore.IsUnity6000OrNewer; + private static bool UseUnity6000Fallbacks => DataCenterCompatibility.SafeModeActive; public static bool ShowMenu { @@ -92,7 +93,7 @@ internal static void InitUI() CreateTopNavBar(); // This could be automated with Assembly.GetTypes(), // but the order is important and I'd have to write something to handle the order. - if (UseUnity6000Fallbacks) + if (DataCenterCompatibility.DeferPanelBootstrap) { ExplorerCore.Log($"UI Stage: Enable deferred panel bootstrap on Unity {Application.unityVersion}"); RegisterDeferredPanels(); @@ -115,7 +116,7 @@ internal static void InitUI() // Call some initialize methods Notification.Init(UIRoot); - if (UseUnity6000Fallbacks) + if (DataCenterCompatibility.DeferPanelBootstrap) ExplorerCore.Log($"UI Stage: Skip ConsoleController.Init until CSConsole is opened on Unity {Application.unityVersion}"); else InitializeConsoleController(); @@ -285,7 +286,7 @@ private static void CreateTopNavBar() UIFactory.SetLayoutGroup(NavbarTabButtonHolder, false, true, true, true, 4, 2, 2, 2, 2); // Time scale widget - if (UseUnity6000Fallbacks) + if (DataCenterCompatibility.DisableTimeScaleWidget) ExplorerCore.Log($"UI Stage: Navbar - Skip TimeScaleWidget on Unity {Application.unityVersion}"); else TimeScaleWidget.SetUp(navbarPanel); diff --git a/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs b/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs index 41033ab..2ec4c97 100644 --- a/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs +++ b/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs @@ -1,6 +1,7 @@ using UniverseLib.UI; using UniverseLib.UI.Models; using UniverseLib.UI.ObjectPool; +using UnityExplorer.Runtime; namespace UnityExplorer.UI.Widgets { @@ -117,7 +118,7 @@ internal void ConstructUI(GameObject parent) Title = UIFactory.CreateLabel(UIRoot, "Title", LocalizationManager.GetText("generic_arguments", "Generic Arguments"), TextAnchor.MiddleCenter); UIFactory.SetLayoutElement(Title.gameObject, minHeight: 25, flexibleWidth: 9999); - if (ExplorerCore.IsUnity6000OrNewer) + if (DataCenterCompatibility.UseSafeScrollFallbacks) { ExplorerCore.Log("Unity 6000 detected, using safe Generic Arguments container fallback."); diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 index 040c00e..4e9e6a8 100644 --- a/tests/datacenter-tooling.tests.ps1 +++ b/tests/datacenter-tooling.tests.ps1 @@ -28,13 +28,13 @@ $Readme = Read-RepoFile "README.md" $Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" Assert-Contains "installer supports auto-detect helper" $Installer "Find-DataCenterGameDir" -Assert-Contains "installer supports status mode" $Installer "\[switch\]\$Status" -Assert-Contains "installer supports uninstall mode" $Installer "\[switch\]\$Uninstall" -Assert-Contains "installer supports restore backup mode" $Installer "\[string\]\$RestoreBackup" +Assert-Contains "installer supports status mode" $Installer '\[switch\]\$Status' +Assert-Contains "installer supports uninstall mode" $Installer '\[switch\]\$Uninstall' +Assert-Contains "installer supports restore backup mode" $Installer '\[string\]\$RestoreBackup' Assert-Contains "installer reports install state" $Installer "Get-InstallState" -Assert-Contains "installer can open logs" $Installer "\[switch\]\$OpenLogs" +Assert-Contains "installer can open logs" $Installer '\[switch\]\$OpenLogs' -Assert-Contains "release workflow checks existing tags" $Workflow "gh release view \$tag" +Assert-Contains "release workflow checks existing tags" $Workflow 'gh release view \$tag' Assert-Contains "release workflow validates zip contents" $Workflow "Expand-Archive" Assert-Contains "release workflow reads changelog" $Workflow "CHANGELOG.md" diff --git a/update-datacenter-known-issues.ps1 b/update-datacenter-known-issues.ps1 new file mode 100644 index 0000000..891e0c2 --- /dev/null +++ b/update-datacenter-known-issues.ps1 @@ -0,0 +1,88 @@ +param( + [string]$GameDir = "C:\Program Files (x86)\Steam\steamapps\common\Data Center", + [string]$OutputPath = (Join-Path $PSScriptRoot "docs\DATACENTER_KNOWN_ISSUES.md") +) + +$ErrorActionPreference = "Stop" + +$ResolvedGameDir = Resolve-Path -LiteralPath $GameDir -ErrorAction SilentlyContinue +if ($ResolvedGameDir) { + $GameDir = $ResolvedGameDir.Path +} + +$LatestLog = Join-Path $GameDir "MelonLoader\Latest.log" + +function Select-LogValue { + param( + [string]$Pattern, + [string]$Fallback + ) + + if (!(Test-Path -LiteralPath $LatestLog)) { + return $Fallback + } + + $Match = Select-String -LiteralPath $LatestLog -Pattern $Pattern | Select-Object -Last 1 + if ($Match) { + return $Match.Line.Trim() + } + + return $Fallback +} + +$GeneratedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz" +$MelonLoaderLine = Select-LogValue "MelonLoader v" "MelonLoader: " +$UnityLine = Select-LogValue "Unity Version:" "Unity Version: " +$GameVersionLine = Select-LogValue "Game Version:" "Game Version: " + +$Lines = @( + "# Data Center Known Issues", + "", + "Generated: $GeneratedAt", + "", + "## Detected Runtime", + "", + "- $UnityLine", + "- $MelonLoaderLine", + "- $GameVersionLine", + "- Game directory: $GameDir", + "", + "## Support Matrix", + "", + "| Area | Status | Notes |", + "|---|---|---|", + "| Unity 6000.x / IL2CPP / CoreCLR startup | Supported | Tested with Unity 6000.4.12f1 and MelonLoader 0.7.3 Open-Beta. |", + "| C# Console | Supported with fallback | Uses safe multi-line input on Unity 6000. |", + "| Hook Manager | Supported with fallback | Uses safe editor input and generic argument container on Unity 6000. |", + "| Object Search | Supported | Preferred replacement for Scene Explorer in Data Center. |", + "| Scene Explorer | Disabled by default | Disabled because SceneManager scene access can throw AccessViolationException on Unity 6000 IL2CPP/CoreCLR. |", + "| TimeScale widget | Disabled by default | Can be re-enabled from options if needed, but restart is recommended. |", + "| UniverseLib UI AssetBundle | Disabled by default | Built-in UI resources are used on Unity 6000. |", + "", + "## Known Issues", + "", + '- MelonLoader can log an early `EndOfStreamException` while reading game info. If UnityExplorer initializes afterwards, this is not currently blocking.', + "- Some Unity UIElements generic type cache warnings can appear during UniverseLib startup. They are logged as warnings and have not blocked the Data Center flow.", + "- Layer name lookup is avoided in Unity 6000 safe paths; numeric layer labels may appear instead.", + '- If UnityExplorer does not load, run `.\install-datacenter.ps1 -Status` and check for duplicate or misplaced DLLs.', + "", + "## Useful Commands", + "", + '```powershell', + '.\install-datacenter.ps1 -Status', + '.\install-datacenter.ps1 -BuildIfMissing -DryRun', + '.\install-datacenter.ps1 -Uninstall', + '.\install-datacenter.ps1 -RestoreBackup latest', + '.\collect-datacenter-logs.ps1', + '.\update-datacenter-known-issues.ps1', + '```' +) + +$OutputDir = Split-Path -Parent $OutputPath +if (![string]::IsNullOrWhiteSpace($OutputDir)) { + New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null +} + +Set-Content -LiteralPath $OutputPath -Value $Lines -Encoding utf8 +Write-Host "Updated Data Center known issues:" +Write-Host $OutputPath From 5fb0e44eb7111a6863f35236eaf6cd9ec461c551 Mon Sep 17 00:00:00 2001 From: Bloody Date: Fri, 10 Jul 2026 21:35:14 +0200 Subject: [PATCH 3/9] fix: guard empty Object Search result lists --- UniverseLib/src/UI/Widgets/ScrollView/ScrollPool.cs | 3 +++ src/ObjectExplorer/ObjectSearch.cs | 12 ++++++++++++ tests/datacenter-tooling.tests.ps1 | 2 ++ 3 files changed, 17 insertions(+) diff --git a/UniverseLib/src/UI/Widgets/ScrollView/ScrollPool.cs b/UniverseLib/src/UI/Widgets/ScrollView/ScrollPool.cs index 9ad3054..d7df60b 100644 --- a/UniverseLib/src/UI/Widgets/ScrollView/ScrollPool.cs +++ b/UniverseLib/src/UI/Widgets/ScrollView/ScrollPool.cs @@ -698,6 +698,9 @@ private void OnSliderValueChanged(float val) if (!HasValidPoolState()) return; + if (DataSource.ItemCount <= 0 || HeightCache.Count <= 0) + return; + RefreshCellHeightsFast(); // normalize the scroll position for the scroll bounds. diff --git a/src/ObjectExplorer/ObjectSearch.cs b/src/ObjectExplorer/ObjectSearch.cs index 954a730..ba2fe4d 100644 --- a/src/ObjectExplorer/ObjectSearch.cs +++ b/src/ObjectExplorer/ObjectSearch.cs @@ -165,6 +165,12 @@ private void OnTypeInputChanged(string val) public void SetCell(ButtonCell cell, int index) { + if (index < 0 || index >= currentResults.Count) + { + cell.Disable(); + return; + } + if (!cachedCellTexts.ContainsKey(index)) { string text; @@ -184,6 +190,12 @@ public void SetCell(ButtonCell cell, int index) private void OnCellClicked(int dataIndex) { + if (dataIndex < 0 || dataIndex >= currentResults.Count) + { + ExplorerCore.LogWarning("Object Search ignored stale result click at index " + dataIndex + "."); + return; + } + AddRecent(currentResults[dataIndex]); if (context == SearchContext.Class) diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 index 4e9e6a8..8a7f14a 100644 --- a/tests/datacenter-tooling.tests.ps1 +++ b/tests/datacenter-tooling.tests.ps1 @@ -26,6 +26,7 @@ $Installer = Read-RepoFile "install-datacenter.ps1" $Workflow = Read-RepoFile ".github\workflows\release-datacenter-coreclr.yml" $Readme = Read-RepoFile "README.md" $Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" +$ScrollPool = Read-RepoFile "UniverseLib\src\UI\Widgets\ScrollView\ScrollPool.cs" Assert-Contains "installer supports auto-detect helper" $Installer "Find-DataCenterGameDir" Assert-Contains "installer supports status mode" $Installer '\[switch\]\$Status' @@ -44,5 +45,6 @@ Assert-Contains "readme documents support package" $Readme "collect-datacenter-l Assert-Contains "troubleshooting documents status command" $Troubleshooting "-Status" Assert-Contains "troubleshooting documents duplicated files" $Troubleshooting "doppelt|duplicate" +Assert-Contains "scroll pool guards empty data source" $ScrollPool "DataSource\.ItemCount <= 0" Write-Host "All Data Center tooling tests passed." From b19a0c45344c54041d57e763004cea745f77edbd Mon Sep 17 00:00:00 2001 From: Bloody Date: Fri, 10 Jul 2026 22:05:48 +0200 Subject: [PATCH 4/9] test: add deferred inspector panel regression --- tests/datacenter-tooling.tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 index 8a7f14a..2358718 100644 --- a/tests/datacenter-tooling.tests.ps1 +++ b/tests/datacenter-tooling.tests.ps1 @@ -27,6 +27,7 @@ $Workflow = Read-RepoFile ".github\workflows\release-datacenter-coreclr.yml" $Readme = Read-RepoFile "README.md" $Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" $ScrollPool = Read-RepoFile "UniverseLib\src\UI\Widgets\ScrollView\ScrollPool.cs" +$UIManager = Read-RepoFile "src\UI\UIManager.cs" Assert-Contains "installer supports auto-detect helper" $Installer "Find-DataCenterGameDir" Assert-Contains "installer supports status mode" $Installer '\[switch\]\$Status' @@ -46,5 +47,6 @@ Assert-Contains "readme documents support package" $Readme "collect-datacenter-l Assert-Contains "troubleshooting documents status command" $Troubleshooting "-Status" Assert-Contains "troubleshooting documents duplicated files" $Troubleshooting "doppelt|duplicate" Assert-Contains "scroll pool guards empty data source" $ScrollPool "DataSource\.ItemCount <= 0" +Assert-Contains "deferred SetPanelActive reports missing panel creation failure" $UIManager "SetPanelActive could not create deferred panel" Write-Host "All Data Center tooling tests passed." From 210da7cd15f29bd5678e1c80789bdb853944f39f Mon Sep 17 00:00:00 2001 From: Bloody Date: Fri, 10 Jul 2026 22:06:50 +0200 Subject: [PATCH 5/9] fix: create deferred inspector panel on inspect --- src/UI/UIManager.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/UI/UIManager.cs b/src/UI/UIManager.cs index 801a8fd..09fdb9c 100644 --- a/src/UI/UIManager.cs +++ b/src/UI/UIManager.cs @@ -186,6 +186,18 @@ public static void TogglePanel(Panels panel) public static void SetPanelActive(Panels panelType, bool active) { + if (!UIPanels.ContainsKey(panelType)) + { + if (!active) + return; + + if (!EnsurePanelCreated(panelType)) + { + ExplorerCore.LogWarning($"SetPanelActive could not create deferred panel {panelType}."); + return; + } + } + GetPanel(panelType).SetActive(active); } From f1b2785f465fb109f9223b9f08331114de8d0569 Mon Sep 17 00:00:00 2001 From: Bloody Date: Sat, 11 Jul 2026 02:42:34 +0200 Subject: [PATCH 6/9] test: add world hover target label coverage --- tests/datacenter-tooling.tests.ps1 | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 index 2358718..2b4c62c 100644 --- a/tests/datacenter-tooling.tests.ps1 +++ b/tests/datacenter-tooling.tests.ps1 @@ -22,12 +22,28 @@ function Assert-Contains { Write-Host "PASS: $Name" } +function Assert-FileExists { + param( + [string]$Name, + [string]$Path + ) + + if (-not (Test-Path -LiteralPath (Join-Path $RepoRoot $Path))) { + throw "FAIL: $Name file did not exist: $Path" + } + + Write-Host "PASS: $Name" +} + $Installer = Read-RepoFile "install-datacenter.ps1" $Workflow = Read-RepoFile ".github\workflows\release-datacenter-coreclr.yml" $Readme = Read-RepoFile "README.md" $Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" $ScrollPool = Read-RepoFile "UniverseLib\src\UI\Widgets\ScrollView\ScrollPool.cs" $UIManager = Read-RepoFile "src\UI\UIManager.cs" +$ConfigManager = Read-RepoFile "src\Config\ConfigManager.cs" +$OptionsPanel = Read-RepoFile "src\UI\Panels\OptionsPanel.cs" +$WorldInspector = Read-RepoFile "src\Inspectors\MouseInspectors\WorldInspector.cs" Assert-Contains "installer supports auto-detect helper" $Installer "Find-DataCenterGameDir" Assert-Contains "installer supports status mode" $Installer '\[switch\]\$Status' @@ -48,5 +64,15 @@ Assert-Contains "troubleshooting documents status command" $Troubleshooting "-St Assert-Contains "troubleshooting documents duplicated files" $Troubleshooting "doppelt|duplicate" Assert-Contains "scroll pool guards empty data source" $ScrollPool "DataSource\.ItemCount <= 0" Assert-Contains "deferred SetPanelActive reports missing panel creation failure" $UIManager "SetPanelActive could not create deferred panel" +Assert-Contains "config exposes world hover target label" $ConfigManager "World_Hover_Label" +Assert-Contains "options exposes hover target toggle" $OptionsPanel "Show target name while aiming" +Assert-Contains "ui manager initializes world hover label" $UIManager "WorldHoverLabel\.Init\(UIRoot\)" +Assert-Contains "ui manager updates world hover label" $UIManager "WorldHoverLabel\.Update\(\)" +Assert-Contains "world inspector exposes aim target lookup" $WorldInspector "TryFindAimTarget" + +Assert-FileExists "world hover label source exists" "src\UI\WorldHoverLabel.cs" +$WorldHoverLabel = Read-RepoFile "src\UI\WorldHoverLabel.cs" +Assert-Contains "world hover label uses aim target lookup" $WorldHoverLabel "WorldInspector\.TryFindAimTarget" +Assert-Contains "world hover label does not block raycasts" $WorldHoverLabel "blocksRaycasts = false" Write-Host "All Data Center tooling tests passed." From e1100f746a89015bd64481cef6bbc38aacce2b25 Mon Sep 17 00:00:00 2001 From: Bloody Date: Sat, 11 Jul 2026 02:46:08 +0200 Subject: [PATCH 7/9] feat: show aimed world object name --- src/Config/ConfigManager.cs | 5 + .../MouseInspectors/WorldInspector.cs | 70 +++++--- src/UI/Panels/OptionsPanel.cs | 52 +++++- src/UI/UIManager.cs | 6 + src/UI/WorldHoverLabel.cs | 149 ++++++++++++++++++ 5 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 src/UI/WorldHoverLabel.cs diff --git a/src/Config/ConfigManager.cs b/src/Config/ConfigManager.cs index 37ded8f..cdaf4d9 100644 --- a/src/Config/ConfigManager.cs +++ b/src/Config/ConfigManager.cs @@ -36,6 +36,7 @@ public static class ConfigManager public static ConfigElement Menu_Language; public static ConfigElement World_MouseInspect_Keybind; public static ConfigElement UI_MouseInspect_Keybind; + public static ConfigElement World_Hover_Label; public static ConfigElement Unity6000_EnableSafeMode; public static ConfigElement Unity6000_DisableSceneExplorer; public static ConfigElement Unity6000_UseSafeInputFallbacks; @@ -167,6 +168,10 @@ private static void CreateConfigElements() "Optional keybind to begin a UI-mode Mouse Inspect.", KeyCode.None); + World_Hover_Label = new("World Hover Target Label", + "Show a small non-interactive label with the aimed world object's name while UnityExplorer is open.", + true); + Unity6000_EnableSafeMode = new("Unity 6000 Safe Mode", "Enable Data Center compatibility fallbacks for Unity 6000 / IL2CPP / CoreCLR. Restart recommended when changing this setting.", true); diff --git a/src/Inspectors/MouseInspectors/WorldInspector.cs b/src/Inspectors/MouseInspectors/WorldInspector.cs index 568a740..1e00bcd 100644 --- a/src/Inspectors/MouseInspectors/WorldInspector.cs +++ b/src/Inspectors/MouseInspectors/WorldInspector.cs @@ -103,6 +103,25 @@ public override void UpdateMouseInspect(Vector2 mousePos) MouseInspector.Instance.ClearHitData(); } + internal static bool TryFindAimTarget([NotNullWhen(true)] out GameObject? result, out string source) + { + result = null; + source = string.Empty; + + Camera? camera = EnsureMainCamera(updateInspectorTitle: false, logWhenMissing: false); + if (camera == null) + return false; + + if (TryFindObjectAtScreenPosition(camera, GetScreenCenter(), out GameObject? centerObject)) + { + result = centerObject; + source = "center aim"; + return true; + } + + return false; + } + private static Vector2 GetScreenCenter() { return new Vector2(Screen.width * 0.5f, Screen.height * 0.5f); @@ -695,41 +714,48 @@ public override void OnEndInspect() } - private static Camera? EnsureMainCamera() + private static Camera? EnsureMainCamera(bool updateInspectorTitle = true, bool logWhenMissing = true) { if (MainCamera != null) { - try - { - MouseInspector.Instance.UpdateInspectorTitle( - $"World Inspector ({MainCamera.name}) (press ESC to cancel)" - ); - } - catch - { - } + UpdateInspectorCameraTitle(MainCamera, updateInspectorTitle); return MainCamera; } - if (TryGetValidMainCamera(out var camera)) + if (TryGetValidMainCamera(out var camera, logWhenMissing)) { MainCamera = camera; - MouseInspector.Instance.UpdateInspectorTitle( - $"World Inspector ({camera.name}) (press ESC to cancel)" - ); + UpdateInspectorCameraTitle(camera, updateInspectorTitle); ExplorerCore.Log($"Using '{camera.transform.GetTransformPath(true)}'"); return camera; } return null; } + private static void UpdateInspectorCameraTitle(Camera camera, bool updateInspectorTitle) + { + if (!updateInspectorTitle || MouseInspector.Instance == null) + return; + + try + { + MouseInspector.Instance.UpdateInspectorTitle( + $"World Inspector ({camera.name}) (press ESC to cancel)" + ); + } + catch + { + } + } + private static bool TryGetValidMainCamera( #if NETFRAMEWORK - out Camera camera) + out Camera camera, #else - [NotNullWhen(true)] out Camera? camera) + [NotNullWhen(true)] out Camera? camera, #endif + bool logWhenMissing = true) { camera = Camera.main; if (camera != null) @@ -737,7 +763,9 @@ private static bool TryGetValidMainCamera( return true; } - ExplorerCore.LogWarning("No Camera.main found, trying to find a camera named 'Main Camera' or 'MainCamera'..."); + if (logWhenMissing) + ExplorerCore.LogWarning("No Camera.main found, trying to find a camera named 'Main Camera' or 'MainCamera'..."); + camera = Camera.allCameras.FirstOrDefault( c => c.name == "Main Camera" || c.name == "MainCamera"); if (camera != null) @@ -745,7 +773,9 @@ private static bool TryGetValidMainCamera( return true; } - ExplorerCore.LogWarning("No camera named 'Main Camera' or 'MainCamera' found, using the first camera created..."); + if (logWhenMissing) + ExplorerCore.LogWarning("No camera named 'Main Camera' or 'MainCamera' found, using the first camera created..."); + camera = Camera.allCameras.FirstOrDefault(); if (camera != null) { @@ -753,7 +783,9 @@ private static bool TryGetValidMainCamera( } // If we reach here, no cameras were found at all. - ExplorerCore.LogWarning("No valid main cameras found!"); + if (logWhenMissing) + ExplorerCore.LogWarning("No valid main cameras found!"); + return false; } } diff --git a/src/UI/Panels/OptionsPanel.cs b/src/UI/Panels/OptionsPanel.cs index 76999d6..9f14092 100644 --- a/src/UI/Panels/OptionsPanel.cs +++ b/src/UI/Panels/OptionsPanel.cs @@ -195,7 +195,7 @@ private void CreateMouseInspectKeybindDropdowns() new Vector4(6, 6, 6, 6), new Color(0.08f, 0.08f, 0.08f)); - UIFactory.SetLayoutElement(block, minHeight: 110, flexibleHeight: 0, flexibleWidth: 9999); + UIFactory.SetLayoutElement(block, minHeight: 142, flexibleHeight: 0, flexibleWidth: 9999); Text header = UIFactory.CreateLabel( block, @@ -215,6 +215,8 @@ private void CreateMouseInspectKeybindDropdowns() hint.color = new Color(0.75f, 0.75f, 0.75f); UIFactory.SetLayoutElement(hint.gameObject, minHeight: 24, flexibleWidth: 9999, flexibleHeight: 0); + CreateWorldHoverTargetToggle(block); + CreateMouseInspectKeybindRow( block, "World Inspect", @@ -228,6 +230,54 @@ private void CreateMouseInspectKeybindDropdowns() KeyCode.F9); } + private void CreateWorldHoverTargetToggle(GameObject parent) + { + GameObject row = UIFactory.CreateHorizontalGroup( + parent, + "WorldHoverTargetLabelRow", + true, + false, + true, + true, + 6, + new Vector4(0, 0, 0, 0)); + + UIFactory.SetLayoutElement(row, minHeight: 28, flexibleHeight: 0, flexibleWidth: 9999); + + Text labelText = UIFactory.CreateLabel( + row, + "WorldHoverTargetLabelText", + "Show target name while aiming", + TextAnchor.MiddleLeft); + + UIFactory.SetLayoutElement(labelText.gameObject, minWidth: 220, minHeight: 24, flexibleWidth: 9999, flexibleHeight: 0); + + GameObject toggleObj = UIFactory.CreateToggle( + row, + "WorldHoverTargetLabelToggle", + out Toggle toggle, + out Text toggleText); + + toggle.isOn = ConfigManager.World_Hover_Label.Value; + toggleText.text = toggle.isOn ? "On" : "Off"; + + UIFactory.SetLayoutElement(toggleObj, minWidth: 90, minHeight: 24, flexibleWidth: 0, flexibleHeight: 0); + UIFactory.SetToggleValueChangedListener(toggleObj, toggle, value => + { + ConfigManager.World_Hover_Label.Value = value; + toggleText.text = value ? "On" : "Off"; + + try + { + ConfigManager.Handler.SaveConfig(); + } + catch (Exception ex) + { + ExplorerCore.LogWarning("Failed to save World Hover Target Label setting: " + ex.Message); + } + }); + } + private void CreateMouseInspectKeybindRow( GameObject parent, string label, diff --git a/src/UI/UIManager.cs b/src/UI/UIManager.cs index 09fdb9c..12d3d2e 100644 --- a/src/UI/UIManager.cs +++ b/src/UI/UIManager.cs @@ -116,6 +116,7 @@ internal static void InitUI() // Call some initialize methods Notification.Init(UIRoot); + WorldHoverLabel.Init(UIRoot); if (DataCenterCompatibility.DeferPanelBootstrap) ExplorerCore.Log($"UI Stage: Skip ConsoleController.Init until CSConsole is opened on Unity {Application.unityVersion}"); else @@ -140,7 +141,12 @@ public static void Update() // If we are doing a Mouse Inspect, we don't need to update anything else. if (MouseInspector.Instance != null && MouseInspector.Instance.TryUpdate()) + { + WorldHoverLabel.Hide(); return; + } + + WorldHoverLabel.Update(); // Update Notification modal Notification.Update(); diff --git a/src/UI/WorldHoverLabel.cs b/src/UI/WorldHoverLabel.cs new file mode 100644 index 0000000..20f13d7 --- /dev/null +++ b/src/UI/WorldHoverLabel.cs @@ -0,0 +1,149 @@ +using UnityExplorer.Config; +using UnityExplorer.Inspectors; +using UnityExplorer.Inspectors.MouseInspectors; +using UniverseLib.UI; + +#nullable enable + +namespace UnityExplorer.UI; + +public static class WorldHoverLabel +{ + private const float UpdateInterval = 0.15f; + private const float VerticalOffset = 48f; + + private static GameObject? root; + private static Text? targetText; + private static CanvasGroup? canvasGroup; + private static float timeOfLastUpdate; + private static GameObject? lastTarget; + + public static void Init(GameObject parent) + { + if (root != null) + return; + + root = UIFactory.CreateUIObject("WorldHoverTargetLabel", parent); + RectTransform rect = root.GetComponent(); + rect.pivot = new Vector2(0.5f, 0.5f); + rect.sizeDelta = new Vector2(560f, 34f); + + Image background = root.AddComponent(); + background.color = new Color(0.03f, 0.03f, 0.03f, 0.78f); + background.raycastTarget = false; + + UIFactory.SetLayoutGroup( + root, + true, + true, + true, + true, + 0, + 8, + 8, + 4, + 4, + TextAnchor.MiddleCenter); + + targetText = UIFactory.CreateLabel( + root, + "WorldHoverTargetText", + string.Empty, + TextAnchor.MiddleCenter, + Color.white, + true, + 14); + + targetText.horizontalOverflow = HorizontalWrapMode.Wrap; + targetText.verticalOverflow = VerticalWrapMode.Truncate; + targetText.gameObject.AddComponent(); + targetText.raycastTarget = false; + UIFactory.SetLayoutElement(targetText.gameObject, minHeight: 24, flexibleWidth: 9999, flexibleHeight: 0); + + canvasGroup = root.AddComponent(); + canvasGroup.blocksRaycasts = false; + canvasGroup.interactable = false; + + Hide(); + } + + public static void Update() + { + if (root == null || targetText == null) + return; + + if (!ConfigManager.World_Hover_Label.Value || MouseInspector.Inspecting) + { + Hide(); + return; + } + + if (!timeOfLastUpdate.OccuredEarlierThan(UpdateInterval)) + return; + + timeOfLastUpdate = Time.realtimeSinceStartup; + + try + { + if (WorldInspector.TryFindAimTarget(out GameObject? target, out string source)) + { + Show(target, source); + return; + } + } + catch (Exception ex) + { + ExplorerCore.LogWarning($"WorldHoverLabel update failed: {ex.Message}"); + } + + Hide(); + } + + public static void Hide() + { + lastTarget = null; + + if (targetText != null) + targetText.text = string.Empty; + + if (canvasGroup != null) + canvasGroup.alpha = 0f; + + if (root != null) + root.SetActive(false); + } + + private static void Show(GameObject target, string source) + { + if (root == null || targetText == null || target == null || !target) + return; + + PositionNearAimPoint(); + + if (target != lastTarget) + { + string sourceLabel = string.IsNullOrEmpty(source) + ? string.Empty + : $" ({source})"; + + targetText.text = $"Target:{sourceLabel} {target.name}"; + lastTarget = target; + } + + if (canvasGroup != null) + canvasGroup.alpha = 1f; + + if (!root.activeSelf) + root.SetActive(true); + } + + private static void PositionNearAimPoint() + { + if (root == null || UIManager.UIRootRect == null) + return; + + Vector3 screenCenter = new(Screen.width * 0.5f, Screen.height * 0.5f, 0f); + Vector3 inversePos = UIManager.UIRootRect.InverseTransformPoint(screenCenter); + root.transform.localPosition = new Vector3(inversePos.x, inversePos.y + VerticalOffset, 0f); + } +} From 8c109f3398daa6843ba3ce215f99e6b3621e43bb Mon Sep 17 00:00:00 2001 From: Bloody Date: Sat, 11 Jul 2026 08:49:49 +0200 Subject: [PATCH 8/9] test: require hover target label documentation --- tests/datacenter-tooling.tests.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/datacenter-tooling.tests.ps1 b/tests/datacenter-tooling.tests.ps1 index 2b4c62c..8d8990b 100644 --- a/tests/datacenter-tooling.tests.ps1 +++ b/tests/datacenter-tooling.tests.ps1 @@ -38,6 +38,7 @@ function Assert-FileExists { $Installer = Read-RepoFile "install-datacenter.ps1" $Workflow = Read-RepoFile ".github\workflows\release-datacenter-coreclr.yml" $Readme = Read-RepoFile "README.md" +$Changelog = Read-RepoFile "CHANGELOG.md" $Troubleshooting = Read-RepoFile "docs\TROUBLESHOOTING_DATACENTER_DE.md" $ScrollPool = Read-RepoFile "UniverseLib\src\UI\Widgets\ScrollView\ScrollPool.cs" $UIManager = Read-RepoFile "src\UI\UIManager.cs" @@ -59,6 +60,8 @@ Assert-Contains "release workflow reads changelog" $Workflow "CHANGELOG.md" Assert-Contains "readme documents uninstall" $Readme "-Uninstall" Assert-Contains "readme documents restore backup" $Readme "-RestoreBackup" Assert-Contains "readme documents support package" $Readme "collect-datacenter-logs.ps1" +Assert-Contains "readme documents hover target label" $Readme "Show target name while aiming" +Assert-Contains "changelog documents hover target label" $Changelog "hover target label|Hover-Zielanzeige" Assert-Contains "troubleshooting documents status command" $Troubleshooting "-Status" Assert-Contains "troubleshooting documents duplicated files" $Troubleshooting "doppelt|duplicate" From 5ba1574462038bf49c2ab3694ab5f224ca7f2bcd Mon Sep 17 00:00:00 2001 From: Bloody Date: Sat, 11 Jul 2026 08:51:48 +0200 Subject: [PATCH 9/9] docs: document world hover target label --- CHANGELOG.md | 2 ++ README.md | 4 ++++ docs/CHANGELOG_DATACENTER_DE_EN.md | 2 ++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f05247..54ad2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this Data Center-focused UnityExplorer fork are documente - Added in-game Data Center runtime status, log opening and support package buttons to the Options panel. - Added configurable Unity 6000 safe-mode switches for scene, input, scroll and TimeScale fallbacks. - Expanded Object Search with tag, layer, component and active-state filters plus recent/favorite working sets. +- Added a hover target label that shows the aimed world object's name without starting full Mouse Inspect. - Added a manual GitHub Actions release workflow with tag guards, ZIP validation and changelog-derived release notes. - Added a generated local Data Center known-issues/support-matrix document. - Centralized Unity 6000 safe UI fallback helpers. @@ -27,6 +28,7 @@ All notable changes to this Data Center-focused UnityExplorer fork are documente - Ingame Data-Center-Runtime-Status, Logs-öffnen und Support-Paket-Buttons im Options-Panel ergänzt. - Konfigurierbare Unity-6000-Safe-Mode-Schalter für Scene-, Input-, Scroll- und TimeScale-Fallbacks hinzugefügt. - Object Search um Tag-, Layer-, Komponenten- und Aktivitätsfilter sowie Recent/Favorites-Arbeitslisten erweitert. +- Hover-Zielanzeige ergänzt, die den Namen des anvisierten World-Objekts ohne vollständigen Mouse Inspect anzeigt. - Manuellen GitHub-Actions-Release-Workflow mit Tag-Prüfung, ZIP-Validierung und Changelog-basierten Release Notes hinzugefügt. - Generierte lokale Data-Center-Known-Issues-/Support-Matrix ergänzt. - Unity-6000-Safe-UI-Fallback-Helfer zentralisiert. diff --git a/README.md b/README.md index 37742b4..4e992d7 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,8 @@ powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 Im Spiel enthält das Options-Panel zusätzlich einen **Data Center Runtime Status** mit Buttons zum Aktualisieren, Logs öffnen und Support-ZIP erstellen. +Das Options-Panel enthält außerdem **Show target name while aiming**. Wenn diese Option aktiv ist, zeigt UnityExplorer beim Zielen auf ein World-Objekt eine kleine nicht-interaktive Zielanzeige mit dem Objektnamen. + --- ## Aus dem Source Code bauen @@ -424,6 +426,8 @@ powershell -ExecutionPolicy Bypass -File .\update-datacenter-known-issues.ps1 In game, the Options panel also includes a **Data Center Runtime Status** block with buttons to refresh status, open logs and create a support ZIP. +The Options panel also includes **Show target name while aiming**. When enabled, UnityExplorer shows a small non-interactive target label with the object name while aiming at a world object. + --- ## Build From Source diff --git a/docs/CHANGELOG_DATACENTER_DE_EN.md b/docs/CHANGELOG_DATACENTER_DE_EN.md index 45ddc81..bc67f82 100644 --- a/docs/CHANGELOG_DATACENTER_DE_EN.md +++ b/docs/CHANGELOG_DATACENTER_DE_EN.md @@ -40,6 +40,7 @@ - Options-Panel um Data-Center-Runtime-Status und Ingame-Support-Paket erweitert. - Unity-6000-Safe-Mode-Schalter konfigurierbar gemacht. - Object Search um Tag-, Layer-, Komponenten- und Aktivitätsfilter sowie Recent/Favorites erweitert. +- Hover-Zielanzeige für den Namen des anvisierten World-Objekts ergänzt. - Lokalen Known-Issues-/Support-Matrix-Generator ergänzt. ## English @@ -82,4 +83,5 @@ - Added Data Center runtime status and in-game support package creation to the Options panel. - Made Unity 6000 safe-mode fallbacks configurable. - Expanded Object Search with tag, layer, component and active-state filters plus recent/favorites. +- Added a hover target label for the aimed world object's name. - Added a local known-issues/support-matrix generator.