diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 8e520c5..91c946d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -43,8 +43,8 @@ body:
id: unity-version
attributes:
label: Unity version
- description: Example / Beispiel: Unity 6000.4.2f1
- placeholder: "Unity 6000.4.2f1"
+ description: Example / Beispiel: Unity 6000.4.12f1
+ placeholder: "Unity 6000.4.12f1"
validations:
required: false
@@ -52,9 +52,9 @@ body:
id: melonloader-version
attributes:
label: MelonLoader version
- description: Example / Beispiel: 0.7.2 Open-Beta
- placeholder: "0.7.2 Open-Beta"
- value: "0.7.2 Open-Beta"
+ description: Example / Beispiel: 0.7.3 Open-Beta
+ placeholder: "0.7.3 Open-Beta"
+ value: "0.7.3 Open-Beta"
validations:
required: true
diff --git a/.github/workflows/release-datacenter-coreclr.yml b/.github/workflows/release-datacenter-coreclr.yml
new file mode 100644
index 0000000..ddc4706
--- /dev/null
+++ b/.github/workflows/release-datacenter-coreclr.yml
@@ -0,0 +1,94 @@
+name: Release Data Center CoreCLR
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: Release tag, for example v1.0.3
+ required: true
+ title:
+ description: Release title
+ required: false
+ prerelease:
+ description: Mark release as prerelease
+ required: false
+ default: "false"
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+
+ - name: Build Data Center CoreCLR
+ shell: pwsh
+ run: .\build-datacenter.ps1
+
+ - name: Prepare release notes
+ shell: pwsh
+ run: |
+ $tag = '${{ inputs.tag }}'
+ $title = '${{ inputs.title }}'
+ if ([string]::IsNullOrWhiteSpace($title)) {
+ $title = "UnityExplorer Data Center Safe Fork $tag"
+ }
+
+ $notes = @(
+ "# $title",
+ "",
+ "Data Center CoreCLR build for Unity 6000.x / IL2CPP / MelonLoader Open-Beta.",
+ "",
+ "## Install",
+ "",
+ "Copy the included folders into your Data Center game directory:",
+ "",
+ '```text',
+ 'Data Center\Mods\UnityExplorer.ML.IL2CPP.CoreCLR.dll',
+ 'Data Center\UserLibs\UniverseLib.ML.IL2CPP.Interop.dll',
+ '```',
+ "",
+ "SHA256 checksums are included in CHECKSUMS_SHA256.txt."
+ )
+
+ Set-Content -LiteralPath release-notes.md -Value $notes -Encoding utf8
+
+ - name: Create GitHub release
+ shell: pwsh
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ $tag = '${{ inputs.tag }}'
+ $title = '${{ inputs.title }}'
+ if ([string]::IsNullOrWhiteSpace($title)) {
+ $title = "UnityExplorer Data Center Safe Fork $tag"
+ }
+
+ $args = @(
+ 'release', 'create', $tag,
+ 'Release\UnityExplorer.MelonLoader.IL2CPP.CoreCLR.zip',
+ 'Release\CHECKSUMS_SHA256.txt',
+ '--title', $title,
+ '--notes-file', 'release-notes.md',
+ '--target', '${{ github.sha }}'
+ )
+
+ if ('${{ inputs.prerelease }}' -eq 'true') {
+ $args += '--prerelease'
+ }
+ else {
+ $args += '--latest'
+ }
+
+ gh @args
diff --git a/.gitignore b/.gitignore
index 813b8e2..23f6bd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ mono_crash.*
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
+SupportPackages/
x64/
x86/
[Aa][Rr][Mm]/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cef438e..034f8f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ 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.
+- 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.
- `build-datacenter.ps1` now writes `Release\CHECKSUMS_SHA256.txt` for the ZIP and installed DLLs.
- Added local bilingual installation and troubleshooting docs for users who do not use the wiki.
@@ -13,6 +18,11 @@ 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.
+- 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.
- `build-datacenter.ps1` schreibt jetzt `Release\CHECKSUMS_SHA256.txt` für ZIP und installierbare DLLs.
- Lokale zweisprachige Installations- und Troubleshooting-Dokumente für Nutzer ohne Wiki ergänzt.
@@ -73,8 +83,8 @@ All notable changes to this Data Center-focused UnityExplorer fork are documente
### Compatibility / Kompatibilität
- Game: Data Center
-- Unity: 6000.4.2f1
-- Loader: MelonLoader v0.7.2 Open-Beta
+- Unity: 6000.x, tested with 6000.4.12f1
+- Loader: MelonLoader v0.7.x Open-Beta, tested with 0.7.3
- Runtime: net6 / CoreCLR
- Game type: IL2CPP
diff --git a/README.md b/README.md
index 8a9eb13..0b8886c 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,10 @@
-
+
-
+
@@ -27,7 +27,7 @@ Ziel dieses Forks ist es, UnityExplorer mit folgender Umgebung nutzbar zu machen
- **Unity 6000**
- **IL2CPP**
- **net6 / CoreCLR**
-- **MelonLoader 0.7.2 Open-Beta**
+- **MelonLoader 0.7.x Open-Beta** (getestet mit 0.7.3)
- **Data Center**
Dieser Fork ist auf Kompatibilität und Stabilität ausgelegt. Einige Standardfunktionen von UnityExplorer können angepasst, verzögert, deaktiviert oder durch sicherere Alternativen ersetzt sein, um Abstürze, Konsolen-Spam oder unsichere Unity-API-Aufrufe zu reduzieren.
@@ -40,10 +40,10 @@ Dieser Fork ist auf Kompatibilität und Stabilität ausgelegt. Einige Standardfu
|---|---|
| Spiel | Data Center |
| Entwickler | Waseku |
-| Unity | 6000.4.2f1 |
+| Unity | 6000.x (getestet mit 6000.4.12f1) |
| Backend | IL2CPP |
| Runtime | net6 / CoreCLR |
-| Loader | MelonLoader 0.7.2 Open-Beta |
+| Loader | MelonLoader 0.7.x Open-Beta (getestet mit 0.7.3) |
| Plattform | Windows |
| Lizenz | GPL-3.0 |
@@ -106,6 +106,13 @@ Wenn du aus diesem Repository baust, kann das Install-Skript die Dateien direkt
powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing
```
+Nützliche Installationsoptionen:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit
+```
+
Ausführliche Anleitung:
[Installationsanleitung](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation)
@@ -135,6 +142,12 @@ Wichtige Log-Datei:
Data Center\MelonLoader\Latest.log
```
+Support-Paket für Fehlersuche erstellen:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
+
---
## Aus dem Source Code bauen
@@ -271,7 +284,7 @@ The goal of this fork is to make UnityExplorer usable with the following environ
- **Unity 6000**
- **IL2CPP**
- **net6 / CoreCLR**
-- **MelonLoader 0.7.2 Open-Beta**
+- **MelonLoader 0.7.x Open-Beta** (tested with 0.7.3)
- **Data Center**
This fork focuses on compatibility and stability. Some default UnityExplorer behavior may be changed, delayed, disabled or replaced with safer alternatives to reduce crashes, console spam and unsafe Unity API calls.
@@ -284,10 +297,10 @@ This fork focuses on compatibility and stability. Some default UnityExplorer beh
|---|---|
| Game | Data Center |
| Developer | Waseku |
-| Unity | 6000.4.2f1 |
+| Unity | 6000.x (tested with 6000.4.12f1) |
| Backend | IL2CPP |
| Runtime | net6 / CoreCLR |
-| Loader | MelonLoader 0.7.2 Open-Beta |
+| Loader | MelonLoader 0.7.x Open-Beta (tested with 0.7.3) |
| Platform | Windows |
| License | GPL-3.0 |
@@ -350,6 +363,13 @@ If you build from this repository, the install script can copy the files directl
powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing
```
+Useful install options:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit
+```
+
Detailed instructions:
[Installation Guide](https://github.com/BloddyMichi/Unity-Explorer-/wiki/Installation)
@@ -379,6 +399,12 @@ Important log file:
Data Center\MelonLoader\Latest.log
```
+Create a support package for troubleshooting:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
+
---
## Build From Source
diff --git a/collect-datacenter-logs.ps1 b/collect-datacenter-logs.ps1
new file mode 100644
index 0000000..16fbd36
--- /dev/null
+++ b/collect-datacenter-logs.ps1
@@ -0,0 +1,152 @@
+param(
+ [string]$GameDir = "C:\Program Files (x86)\Steam\steamapps\common\Data Center",
+ [string]$OutputDir = (Join-Path $PSScriptRoot "SupportPackages"),
+ [switch]$IncludePlayerLog
+)
+
+$ErrorActionPreference = "Stop"
+
+$ResolvedGameDir = Resolve-Path -LiteralPath $GameDir -ErrorAction SilentlyContinue
+if ($ResolvedGameDir) {
+ $GameDir = $ResolvedGameDir.Path
+}
+else {
+ throw "Game directory not found: $GameDir. Pass -GameDir with the correct Data Center install path."
+}
+
+New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
+
+$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
+$PackageRoot = Join-Path $OutputDir "DataCenter-UnityExplorer-Support-$Timestamp"
+$ZipPath = "$PackageRoot.zip"
+
+if (Test-Path -LiteralPath $PackageRoot) {
+ Remove-Item -LiteralPath $PackageRoot -Recurse -Force
+}
+
+New-Item -ItemType Directory -Force -Path $PackageRoot | Out-Null
+
+function Copy-IfExists {
+ param(
+ [string]$Source,
+ [string]$DestinationDir
+ )
+
+ if (!(Test-Path -LiteralPath $Source)) {
+ return $false
+ }
+
+ New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null
+ Copy-Item -LiteralPath $Source -Destination (Join-Path $DestinationDir (Split-Path $Source -Leaf)) -Force
+ return $true
+}
+
+function Get-FileHashLine {
+ param([string]$Path)
+
+ if (!(Test-Path -LiteralPath $Path)) {
+ return "missing $Path"
+ }
+
+ try {
+ $Hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
+ return "$Hash $Path"
+ }
+ catch {
+ return "locked_or_unreadable $Path"
+ }
+}
+
+function Select-LogValue {
+ param(
+ [string]$LogPath,
+ [string]$Pattern
+ )
+
+ if (!(Test-Path -LiteralPath $LogPath)) {
+ return ""
+ }
+
+ $Match = Select-String -LiteralPath $LogPath -Pattern $Pattern | Select-Object -Last 1
+ if ($Match) {
+ return $Match.Line.Trim()
+ }
+
+ return ""
+}
+
+$MelonLog = Join-Path $GameDir "MelonLoader\Latest.log"
+$UnityExplorerLogDir = Join-Path $GameDir "Mods\sinai-dev-UnityExplorer\Logs"
+$PlayerLog = Join-Path $env:USERPROFILE "AppData\LocalLow\Waseku\Data Center\Player.log"
+$InstalledModDll = Join-Path $GameDir "Mods\UnityExplorer.ML.IL2CPP.CoreCLR.dll"
+$InstalledUserLib = Join-Path $GameDir "UserLibs\UniverseLib.ML.IL2CPP.Interop.dll"
+
+Copy-IfExists $MelonLog (Join-Path $PackageRoot "MelonLoader") | Out-Null
+
+if (Test-Path -LiteralPath $UnityExplorerLogDir) {
+ $TargetLogDir = Join-Path $PackageRoot "UnityExplorerLogs"
+ New-Item -ItemType Directory -Force -Path $TargetLogDir | Out-Null
+
+ Get-ChildItem -LiteralPath $UnityExplorerLogDir -Filter "*.txt" -File |
+ Sort-Object LastWriteTime -Descending |
+ Select-Object -First 10 |
+ ForEach-Object {
+ Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $TargetLogDir $_.Name) -Force
+ }
+}
+
+if ($IncludePlayerLog) {
+ Copy-IfExists $PlayerLog (Join-Path $PackageRoot "Unity") | Out-Null
+}
+
+$Processes = foreach ($Process in Get-Process) {
+ $ProcessPath = $null
+ try {
+ $ProcessPath = $Process.Path
+ }
+ catch {
+ $ProcessPath = $null
+ }
+
+ if ($ProcessPath -and $ProcessPath.StartsWith($GameDir, [System.StringComparison]::OrdinalIgnoreCase)) {
+ "{0} (PID {1}) {2}" -f $Process.ProcessName, $Process.Id, $ProcessPath
+ }
+}
+
+$SupportInfo = @(
+ "UnityExplorer Data Center support package",
+ "Created: $(Get-Date -Format o)",
+ "GameDir: $GameDir",
+ "",
+ "Detected versions from Latest.log:",
+ " $(Select-LogValue $MelonLog 'MelonLoader v')",
+ " $(Select-LogValue $MelonLog 'Unity Version:')",
+ " $(Select-LogValue $MelonLog 'Game Version:')",
+ "",
+ "Installed file hashes:",
+ " $(Get-FileHashLine $InstalledModDll)",
+ " $(Get-FileHashLine $InstalledUserLib)",
+ "",
+ "Running Data Center processes:",
+ ($(if ($Processes) { $Processes | ForEach-Object { " $_" } } else { " " })),
+ "",
+ "Important paths:",
+ " MelonLoader log: $MelonLog",
+ " UnityExplorer logs: $UnityExplorerLogDir",
+ " Player log: $PlayerLog",
+ "",
+ "Notes:",
+ " Player.log is only included when -IncludePlayerLog is used."
+)
+
+Set-Content -LiteralPath (Join-Path $PackageRoot "support-info.txt") -Value $SupportInfo -Encoding utf8
+
+if (Test-Path -LiteralPath $ZipPath) {
+ Remove-Item -LiteralPath $ZipPath -Force
+}
+
+Compress-Archive -Path (Join-Path $PackageRoot "*") -DestinationPath $ZipPath -Force
+Remove-Item -LiteralPath $PackageRoot -Recurse -Force
+
+Write-Host "Created Data Center support package:" -ForegroundColor Green
+Write-Host $ZipPath
diff --git a/docs/CHANGELOG_DATACENTER_DE_EN.md b/docs/CHANGELOG_DATACENTER_DE_EN.md
index ffd42d8..950e59c 100644
--- a/docs/CHANGELOG_DATACENTER_DE_EN.md
+++ b/docs/CHANGELOG_DATACENTER_DE_EN.md
@@ -3,8 +3,8 @@
## Deutsch
- UnityExplorer für **Data Center** angepasst.
-- Kompatibilität für **MelonLoader v0.7.2 Open-Beta** verbessert.
-- Anpassungen für **Unity 6000.4.2f1 / IL2CPP / net6 CoreCLR** eingebaut.
+- Kompatibilität für **MelonLoader v0.7.x Open-Beta** verbessert, getestet mit **0.7.3**.
+- Anpassungen für **Unity 6000.x / IL2CPP / net6 CoreCLR** eingebaut, getestet mit **6000.4.12f1**.
- SafeInspector-Modus hinzugefügt.
- Automatische Property-Auswertung deaktiviert, um Fehler bei `Transform.hasChanged` zu vermeiden.
- `SceneManager.sceneCount`-Zugriffe im Object Explorer entschärft.
@@ -33,12 +33,15 @@
- GitHub Actions Workflow für CoreCLR-Build hinzugefügt.
- Upstream-README separat unter `docs/README_UPSTREAM_UnityExplorer.md` abgelegt.
- Fertiges CoreCLR-Release-ZIP unter `release-assets/` beigelegt.
+- 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.
## English
- Adapted UnityExplorer for **Data Center**.
-- Improved compatibility with **MelonLoader v0.7.2 Open-Beta**.
-- Added adjustments for **Unity 6000.4.2f1 / IL2CPP / net6 CoreCLR**.
+- Improved compatibility with **MelonLoader v0.7.x Open-Beta**, tested with **0.7.3**.
+- Added adjustments for **Unity 6000.x / IL2CPP / net6 CoreCLR**, tested with **6000.4.12f1**.
- Added SafeInspector mode.
- Disabled automatic property evaluation to avoid `Transform.hasChanged` errors.
- Hardened Object Explorer against unsafe `SceneManager.sceneCount` calls.
@@ -67,3 +70,6 @@
- Added GitHub Actions workflow for CoreCLR builds.
- Preserved upstream README as `docs/README_UPSTREAM_UnityExplorer.md`.
- Included ready CoreCLR release ZIP under `release-assets/`.
+- 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.
diff --git a/docs/INSTALL_DATACENTER_DE_EN.md b/docs/INSTALL_DATACENTER_DE_EN.md
index 1355b03..68366a7 100644
--- a/docs/INSTALL_DATACENTER_DE_EN.md
+++ b/docs/INSTALL_DATACENTER_DE_EN.md
@@ -40,6 +40,15 @@ Falls du aus dem Repository baust, kann das Install-Skript die Dateien kopieren
powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing
```
+Nützliche Optionen:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit
+```
+
+`-DryRun` zeigt die geplanten Dateien und SHA256-Hashes, ohne etwas zu kopieren. `-WaitForGameExit` wartet, bis Data Center beendet wurde, bevor Dateien ersetzt werden.
+
Bei einem anderen Installationspfad:
```powershell
@@ -62,6 +71,12 @@ ObjectExplorerPanel: Unity 6000 safe mode skips Scene Explorer...
Object Search (Safe)
```
+Bei Problemen kannst du ein Support-Paket erzeugen:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
+
## English
### Recommended path
@@ -102,6 +117,15 @@ If you build from the repository, the install script can copy the files and back
powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing
```
+Useful options:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -DryRun
+powershell -ExecutionPolicy Bypass -File .\install-datacenter.ps1 -BuildIfMissing -WaitForGameExit
+```
+
+`-DryRun` prints the planned files and SHA256 hashes without copying anything. `-WaitForGameExit` waits until Data Center has exited before replacing files.
+
For a custom install path:
```powershell
@@ -123,3 +147,9 @@ UnityExplorer 4.13.5 (IL2CPP) initialized.
ObjectExplorerPanel: Unity 6000 safe mode skips Scene Explorer...
Object Search (Safe)
```
+
+If something fails, create a support package:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
diff --git a/docs/TROUBLESHOOTING_DATACENTER_DE.md b/docs/TROUBLESHOOTING_DATACENTER_DE.md
index 3519f53..6435622 100644
--- a/docs/TROUBLESHOOTING_DATACENTER_DE.md
+++ b/docs/TROUBLESHOOTING_DATACENTER_DE.md
@@ -38,6 +38,11 @@ Object Search (Safe)
- Prüfe, ob die DLL wirklich im `Mods`-Ordner liegt.
- Prüfe, ob `UniverseLib.ML.IL2CPP.Interop.dll` im `UserLibs`-Ordner liegt.
- Entferne alte UnityExplorer- oder UniverseLib-Dateien aus falschen Ordnern.
+- Erzeuge bei Bedarf ein Support-Paket:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
**Button-Klicks oder Mausfokus wirken unzuverlässig**
@@ -73,6 +78,8 @@ 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.
## English
@@ -112,6 +119,11 @@ Object Search (Safe)
- Check that the UnityExplorer DLL is really in the `Mods` folder.
- Check that `UniverseLib.ML.IL2CPP.Interop.dll` is in the `UserLibs` folder.
- Remove old UnityExplorer or UniverseLib files from wrong folders.
+- Create a support package when needed:
+
+```powershell
+powershell -ExecutionPolicy Bypass -File .\collect-datacenter-logs.ps1
+```
**Button clicks or mouse focus feel unreliable**
@@ -147,3 +159,5 @@ 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.
diff --git a/install-datacenter.ps1 b/install-datacenter.ps1
index 78c6f53..28f41fd 100644
--- a/install-datacenter.ps1
+++ b/install-datacenter.ps1
@@ -1,7 +1,10 @@
param(
[string]$GameDir = "C:\Program Files (x86)\Steam\steamapps\common\Data Center",
[switch]$BuildIfMissing,
- [switch]$SkipBackup
+ [switch]$SkipBackup,
+ [switch]$SkipProcessCheck,
+ [switch]$WaitForGameExit,
+ [switch]$DryRun
)
$ErrorActionPreference = "Stop"
@@ -64,12 +67,144 @@ function Write-InstalledFile {
Write-Host " SHA256 $Hash"
}
-New-Item -ItemType Directory -Force -Path $ModsDir | Out-Null
-New-Item -ItemType Directory -Force -Path $UserLibsDir | Out-Null
+function Write-PlannedFile {
+ param(
+ [string]$Source,
+ [string]$Destination
+ )
+
+ $Hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Source).Hash.ToLowerInvariant()
+ Write-Host " $Source"
+ Write-Host " -> $Destination"
+ Write-Host " SHA256 $Hash"
+}
+
+function Get-DataCenterProcesses {
+ $Processes = @()
+
+ foreach ($Process in Get-Process) {
+ $ProcessPath = $null
+ try {
+ $ProcessPath = $Process.Path
+ }
+ catch {
+ $ProcessPath = $null
+ }
+
+ if ($ProcessPath -and $ProcessPath.StartsWith($GameDir, [System.StringComparison]::OrdinalIgnoreCase)) {
+ $Processes += $Process
+ }
+ }
+
+ return $Processes
+}
+
+function Assert-FileIsReplaceable {
+ param([string]$Path)
+
+ if (!(Test-Path -LiteralPath $Path)) {
+ return
+ }
+
+ $Stream = $null
+ try {
+ $Stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
+ }
+ catch {
+ throw "File is currently locked and cannot be replaced: $Path`nClose Data Center and rerun this installer."
+ }
+ finally {
+ if ($Stream) {
+ $Stream.Dispose()
+ }
+ }
+}
+
+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."
+ }
+
+ $MelonLoaderDll = Join-Path $MelonLoaderDir "net6\MelonLoader.dll"
+ if (!(Test-Path -LiteralPath $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")
+ )
+
+ $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
+ }
+
+ Write-Host " $Path"
+ }
+ }
+
+ if ($FoundAny) {
+ Write-Host "Consider removing these files if UnityExplorer does not load correctly." -ForegroundColor Yellow
+ }
+}
$InstalledModDll = Join-Path $ModsDir "UnityExplorer.ML.IL2CPP.CoreCLR.dll"
$InstalledUserLib = Join-Path $UserLibsDir "UniverseLib.ML.IL2CPP.Interop.dll"
+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)
+ }
+
+ 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)
+ }
+
+ throw "Close Data Center before installing UnityExplorer, or rerun with -WaitForGameExit."
+ }
+ }
+}
+
+Assert-MelonLoaderInstall
+
+New-Item -ItemType Directory -Force -Path $ModsDir | Out-Null
+New-Item -ItemType Directory -Force -Path $UserLibsDir | Out-Null
+
+Write-DuplicateWarnings
+
+Assert-FileIsReplaceable $InstalledModDll
+Assert-FileIsReplaceable $InstalledUserLib
+
+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
+ exit 0
+}
+
Backup-ExistingFile $InstalledModDll
Backup-ExistingFile $InstalledUserLib
diff --git a/src/ExplorerCore.cs b/src/ExplorerCore.cs
index 8c54212..aa9e41e 100644
--- a/src/ExplorerCore.cs
+++ b/src/ExplorerCore.cs
@@ -44,6 +44,7 @@ public static void Init(IExplorerLoader loader)
Loader = loader;
Log($"{NAME} {VERSION} initializing...");
+ LogRuntimeCompatibility();
CheckLegacyExplorerFolder();
Directory.CreateDirectory(ExplorerFolder);
@@ -98,6 +99,19 @@ static void LateInit()
// InspectorManager.Inspect(typeof(Tests.TestClass));
}
+ private static void LogRuntimeCompatibility()
+ {
+ string unityVersion = Application.unityVersion ?? "unknown";
+ string safeMode = IsUnity6000OrNewer ? "enabled" : "disabled";
+
+ Log($"Runtime compatibility: Unity {unityVersion}, Unity 6000 safe mode {safeMode}.");
+
+ if (IsUnity6000OrNewer)
+ {
+ Log("Unity 6000 safe mode will disable or replace unstable scene, scroll-view and input-field paths.");
+ }
+ }
+
internal static void Update()
{
ExplorerKeybind.Update();
diff --git a/src/Hooks/HookCreator.cs b/src/Hooks/HookCreator.cs
index 2d6912a..0f39353 100644
--- a/src/Hooks/HookCreator.cs
+++ b/src/Hooks/HookCreator.cs
@@ -1,6 +1,7 @@
using HarmonyLib;
using UnityExplorer.CSConsole;
using UnityExplorer.Runtime;
+using UnityExplorer.UI;
using UnityExplorer.UI.Panels;
using UnityExplorer.UI.Widgets.AutoComplete;
using UniverseLib.UI;
@@ -348,17 +349,7 @@ private static void ConstructSafeEditorInput(int fontSize)
{
ExplorerCore.Log("Unity 6000 detected, using safe Hook Editor input fallback.");
- EditorInputFallback = UIFactory.CreateInputField(EditorRoot, "EditorInput", "");
- UIFactory.SetLayoutElement(EditorInputFallback.Component.gameObject, minWidth: 100, minHeight: 30, flexibleWidth: 5000, flexibleHeight: 5000);
-
- EditorInputFallback.Component.lineType = InputField.LineType.MultiLineNewline;
- EditorInputFallback.Component.targetGraphic.color = new Color(0.12f, 0.12f, 0.12f);
- EditorInputFallback.Component.textComponent.alignment = TextAnchor.UpperLeft;
- EditorInputFallback.Component.textComponent.fontSize = fontSize;
- EditorInputFallback.Component.textComponent.horizontalOverflow = HorizontalWrapMode.Wrap;
- EditorInputFallback.PlaceholderText.alignment = TextAnchor.UpperLeft;
- EditorInputFallback.PlaceholderText.fontSize = fontSize;
- EditorInputFallback.PlaceholderText.horizontalOverflow = HorizontalWrapMode.Wrap;
+ EditorInputFallback = Unity6000SafeUI.CreateSafeMultiLineInput(EditorRoot, "EditorInput", "", fontSize);
}
}
}
diff --git a/src/Inspectors/MouseInspectors/WorldInspector.cs b/src/Inspectors/MouseInspectors/WorldInspector.cs
index daacdc7..568a740 100644
--- a/src/Inspectors/MouseInspectors/WorldInspector.cs
+++ b/src/Inspectors/MouseInspectors/WorldInspector.cs
@@ -26,9 +26,30 @@ public override void ClearHitData()
public override void OnSelectMouseInspect()
{
GameObject? selected = lastHitObject;
+
+ // Important for Data Center:
+ // The game highlights/uses objects through a center-screen "look at" ray,
+ // while UnityExplorer's mouse position can still be over the UI/last button.
+ // If no object was tracked before the click, do one final center-aim lookup.
if (selected == null)
+ {
+ Camera? camera = EnsureMainCamera();
+ if (camera != null && TryFindObjectAtScreenPosition(camera, GetScreenCenter(), out GameObject? centerSelected))
+ {
+ selected = centerSelected;
+ lastHitObject = selected;
+ ExplorerCore.Log($"WorldInspector: click fallback selected center-aim object '{selected.transform.GetTransformPath(true)}'");
+ }
+ }
+
+ if (selected == null)
+ {
+ ExplorerCore.LogWarning("WorldInspector: click ignored because no object was detected under the mouse or center aim.");
return;
+ }
+ ExplorerCore.Log($"WorldInspector: selected '{selected.transform.GetTransformPath(true)}'");
+ DumpObjectDetails(selected);
RuntimeHelper.StartCoroutine(InspectSelectedObjectNextFrame(selected));
}
@@ -52,7 +73,7 @@ public override void UpdateMouseInspect(Vector2 mousePos)
{
// Attempt to ensure camera each time UpdateMouseInspect is called
// in case something changed or wasn't set initially.
- var camera = EnsureMainCamera();
+ Camera? camera = EnsureMainCamera();
if (camera == null)
{
ExplorerCore.LogWarning("No Main Camera was found, unable to inspect world!");
@@ -60,25 +81,610 @@ public override void UpdateMouseInspect(Vector2 mousePos)
return;
}
- Ray ray = camera.ScreenPointToRay(mousePos);
- Physics.Raycast(ray, out RaycastHit hit, 1000f);
-
- if (hit.transform)
+ // First try the actual UnityExplorer mouse position.
+ if (TryFindObjectAtScreenPosition(camera, mousePos, out GameObject? mouseObject))
{
- OnHitGameObject(hit.transform.gameObject);
+ OnHitGameObject(mouseObject, "mouse");
+ return;
}
- else if (lastHitObject)
+
+ // Data Center is a first-person game: the interact target is normally selected
+ // by the center-screen look ray, not by the OS cursor position. The yellow
+ // outline visible in-game follows this center ray.
+ Vector2 centerPos = GetScreenCenter();
+ if (Vector2.Distance(mousePos, centerPos) > 5f &&
+ TryFindObjectAtScreenPosition(camera, centerPos, out GameObject? centerObject))
{
+ OnHitGameObject(centerObject, "center aim");
+ return;
+ }
+
+ if (lastHitObject)
MouseInspector.Instance.ClearHitData();
+ }
+
+ private static Vector2 GetScreenCenter()
+ {
+ return new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
+ }
+
+ private static bool TryFindObjectAtScreenPosition(Camera camera, Vector2 screenPosition, [NotNullWhen(true)] out GameObject? result)
+ {
+ result = null;
+
+ try
+ {
+ Ray ray = camera.ScreenPointToRay(screenPosition);
+
+ // Data Center special case:
+ // Large environment colliders such as wall/floor/ceiling are often hit first,
+ // while small props like the office computer/monitor only have renderers or
+ // child meshes. Therefore we evaluate BOTH physics and renderer candidates.
+ bool hasPhysicsObject = TryFindBestPhysicsHit(ray, out GameObject? physicsObject);
+ bool hasRendererObject = TryFindRendererUnderMouse(camera, screenPosition, out GameObject? rendererObject);
+
+ if (hasPhysicsObject && hasRendererObject && physicsObject != null && rendererObject != null)
+ {
+ if (ShouldPreferRendererCandidate(physicsObject, rendererObject))
+ {
+ result = PromoteToUsefulParent(rendererObject);
+ return true;
+ }
+
+ result = physicsObject;
+ return true;
+ }
+
+ if (hasPhysicsObject && physicsObject != null)
+ {
+ result = physicsObject;
+ return true;
+ }
+
+ if (hasRendererObject && rendererObject != null)
+ {
+ result = PromoteToUsefulParent(rendererObject);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ ExplorerCore.LogWarning($"WorldInspector screen-position lookup failed: {ex.Message}");
+ }
+
+ return false;
+ }
+
+ private static bool TryFindBestPhysicsHit(Ray ray, [NotNullWhen(true)] out GameObject? result)
+ {
+ result = null;
+
+ try
+ {
+ RaycastHit[] hits = Physics.RaycastAll(ray, 1000f, ~0, QueryTriggerInteraction.Collide);
+ if (hits == null || hits.Length == 0)
+ return false;
+
+ Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
+
+ GameObject? bestObject = null;
+ int bestScore = int.MinValue;
+ float bestDistance = float.MaxValue;
+
+ foreach (RaycastHit hit in hits)
+ {
+ if (!hit.transform)
+ continue;
+
+ GameObject hitObject = hit.transform.gameObject;
+ if (!hitObject || !hitObject.activeInHierarchy)
+ continue;
+
+ GameObject candidate = PromoteToUsefulParent(hitObject);
+ int score = ScoreDataCenterHit(candidate, hitObject);
+
+ if (score > bestScore || (score == bestScore && hit.distance < bestDistance))
+ {
+ bestObject = candidate;
+ bestScore = score;
+ bestDistance = hit.distance;
+ }
+ }
+
+ if (bestObject != null && bestObject)
+ {
+ result = bestObject;
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ ExplorerCore.LogWarning($"WorldInspector physics hit fallback failed: {ex.Message}");
+ }
+
+ return false;
+ }
+
+ private static int ScoreDataCenterHit(GameObject candidate, GameObject originalHit)
+ {
+ int score = 0;
+
+ try
+ {
+ // IMPORTANT for Unity 6000 IL2CPP/CoreCLR:
+ // Do NOT call LayerMask.LayerToName here. In Data Center it can crash inside
+ // UnityEngine.Bindings.OutStringMarshaller.GetStringAndDispose with an
+ // AccessViolationException. Use the numeric layer only.
+ score += ScoreKnownLayer(originalHit.layer);
+ score += ScoreKnownLayer(candidate.layer) / 2;
+
+ if (HasNamedComponent(candidate, "Interact")) score += 700;
+ if (HasNamedComponent(candidate, "UsableObject")) score += 650;
+ if (HasNamedComponent(candidate, "RayLookAt")) score += 620;
+ if (HasNamedComponent(candidate, "Rack")) score += 550;
+ if (HasNamedComponent(candidate, "RackMount")) score += 520;
+ if (HasNamedComponent(candidate, "Server")) score += 500;
+ if (HasNamedComponent(candidate, "NetworkSwitch")) score += 500;
+ if (HasNamedComponent(candidate, "PatchPanel")) score += 500;
+ if (HasNamedComponent(candidate, "RackDoor")) score += 450;
+ if (HasNamedComponent(candidate, "Computer")) score += 430;
+ if (HasNamedComponent(candidate, "Monitor")) score += 420;
+
+ if (candidate.GetComponent()) score += 100;
+ if (candidate.GetComponent()) score += 80;
+ if (candidate.transform.parent == null) score += 10;
+ }
+ catch
+ {
+ }
+
+ return score;
+ }
+
+ private static int ScoreKnownLayer(int layer)
+ {
+ // Known/observed Data Center layers from globalgamemanagers.
+ // This intentionally does not resolve layer names through Unity.
+ return layer switch
+ {
+ 2 => -100, // Ignore Raycast
+ 3 => -1000, // Player in Data Center build
+ 5 => 800, // CableHolder / game interaction layer in this build
+ 6 => 200, // Wall
+ 7 => 900, // OnlyForRayCast
+ 8 => 700, // Trolley
+ 9 => 700, // ObjectOnTrolley
+ _ => 0,
+ };
+ }
+
+ private static string SafeLayerLabel(int layer)
+ {
+ // Avoid LayerMask.LayerToName on this Unity 6000 IL2CPP build.
+ return layer switch
+ {
+ 0 => "Default",
+ 1 => "TransparentFX",
+ 2 => "Ignore Raycast",
+ 3 => "Player",
+ 4 => "Water",
+ 5 => "CableHolder/Interaction",
+ 6 => "Wall",
+ 7 => "OnlyForRayCast",
+ 8 => "Trolley",
+ 9 => "ObjectOnTrolley",
+ _ => "Unknown/Custom",
+ };
+ }
+
+ private static GameObject PromoteToUsefulParent(GameObject hitObject)
+ {
+ try
+ {
+ Transform? current = hitObject.transform;
+ GameObject best = hitObject;
+
+ for (int depth = 0; current != null && depth < 8; depth++)
+ {
+ GameObject go = current.gameObject;
+
+ if (HasNamedComponent(go, "Interact") ||
+ HasNamedComponent(go, "UsableObject") ||
+ HasNamedComponent(go, "Rack") ||
+ HasNamedComponent(go, "RackMount") ||
+ HasNamedComponent(go, "Server") ||
+ HasNamedComponent(go, "NetworkSwitch") ||
+ HasNamedComponent(go, "PatchPanel") ||
+ HasNamedComponent(go, "RackDoor"))
+ {
+ best = go;
+ break;
+ }
+
+ // Avoid LayerMask.LayerToName on Unity 6000 IL2CPP/CoreCLR.
+ if (go.layer == 7 || go.layer == 5)
+ best = go;
+
+ current = current.parent;
+ }
+
+ return best;
+ }
+ catch
+ {
+ return hitObject;
+ }
+ }
+
+ private static bool HasNamedComponent(GameObject go, string typeName)
+ {
+ try
+ {
+ Component[] components = go.GetComponents();
+ foreach (Component component in components)
+ {
+ if (!component)
+ continue;
+
+ Type? type = component.GetType();
+ if (type != null && string.Equals(type.Name, typeName, StringComparison.Ordinal))
+ return true;
+
+ try
+ {
+ string? il2CppName = component.GetIl2CppType()?.Name;
+ if (string.Equals(il2CppName, typeName, StringComparison.Ordinal))
+ return true;
+ }
+ catch
+ {
+ }
+ }
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ private static void DumpObjectDetails(GameObject go)
+ {
+ try
+ {
+ if (!go)
+ {
+ ExplorerCore.LogWarning("WorldInspector Dump: GameObject is null.");
+ return;
+ }
+
+ ExplorerCore.Log("========== WorldInspector Object Dump ==========");
+ ExplorerCore.Log($"Name: {go.name}");
+ ExplorerCore.Log($"Path: {go.transform.GetTransformPath(true)}");
+ ExplorerCore.Log($"Layer: {go.layer} ({SafeLayerLabel(go.layer)})");
+ // Avoid reading go.tag in this diagnostic build; Unity string marshalling is unstable on this game/runtime.
+ ExplorerCore.Log("Tag: ");
+ ExplorerCore.Log($"ActiveSelf: {go.activeSelf}");
+ ExplorerCore.Log($"ActiveInHierarchy: {go.activeInHierarchy}");
+
+ try
+ {
+ ExplorerCore.Log($"Scene: {go.scene.name} / Valid: {go.scene.IsValid()} / Loaded: {go.scene.isLoaded}");
+ }
+ catch (Exception sceneEx)
+ {
+ ExplorerCore.LogWarning($"Scene info failed: {sceneEx.Message}");
+ }
+
+ Component[] components = go.GetComponents();
+ ExplorerCore.Log($"Components on selected object: {components.Length}");
+ foreach (Component component in components)
+ {
+ if (!component)
+ {
+ ExplorerCore.Log(" - ");
+ continue;
+ }
+
+ string componentName = component.GetType().FullName ?? component.GetType().Name;
+ try
+ {
+ componentName = component.GetIl2CppType()?.FullName ?? componentName;
+ }
+ catch
+ {
+ }
+
+ ExplorerCore.Log($" - {componentName}");
+ }
+
+ Collider? ownCollider = go.GetComponent();
+ Renderer? ownRenderer = go.GetComponent();
+ Collider[] childColliders = go.GetComponentsInChildren(true);
+ Renderer[] childRenderers = go.GetComponentsInChildren(true);
+
+ ExplorerCore.Log($"Own Collider: {(ownCollider ? ownCollider.GetType().Name : "none")}");
+ ExplorerCore.Log($"Own Renderer: {(ownRenderer ? ownRenderer.GetType().Name : "none")}");
+ ExplorerCore.Log($"Child Colliders: {childColliders.Length}");
+ ExplorerCore.Log($"Child Renderers: {childRenderers.Length}");
+ ExplorerCore.Log("================================================");
+ }
+ catch (Exception ex)
+ {
+ ExplorerCore.LogWarning($"WorldInspector DumpObjectDetails failed: {ex}");
+ }
+ }
+
+ private static bool TryFindRendererUnderMouse(Camera camera, Vector2 mousePos, [NotNullWhen(true)] out GameObject? result)
+ {
+ result = null;
+
+ try
+ {
+ UnityEngine.Object[] renderers = RuntimeHelper.FindObjectsOfTypeAll(typeof(Renderer));
+
+ float bestAnyScore = float.MaxValue;
+ GameObject? bestAnyObject = null;
+
+ float bestPropScore = float.MaxValue;
+ GameObject? bestPropObject = null;
+
+ const float edgePadding = 18f;
+ const float maxCenterDistance = 120f;
+
+ foreach (UnityEngine.Object obj in renderers)
+ {
+ Renderer? renderer = obj.TryCast();
+ if (renderer == null || !renderer)
+ continue;
+
+ GameObject? go = renderer.gameObject;
+ if (go == null || !go || !go.activeInHierarchy || !renderer.enabled)
+ continue;
+
+ Bounds bounds = renderer.bounds;
+ Vector3 centerScreen = camera.WorldToScreenPoint(bounds.center);
+ if (centerScreen.z <= 0f)
+ continue;
+
+ if (!TryGetScreenBounds(camera, bounds, out Rect screenRect))
+ continue;
+
+ Rect paddedRect = new(
+ screenRect.xMin - edgePadding,
+ screenRect.yMin - edgePadding,
+ screenRect.width + edgePadding * 2f,
+ screenRect.height + edgePadding * 2f);
+
+ bool mouseInsideBounds = paddedRect.Contains(mousePos);
+ float centerDistance = Vector2.Distance(new Vector2(centerScreen.x, centerScreen.y), mousePos);
+
+ if (!mouseInsideBounds && centerDistance > maxCenterDistance)
+ continue;
+
+ float projectedArea = Math.Max(1f, Math.Abs(screenRect.width * screenRect.height));
+
+ // Prefer small visible props directly under the cursor.
+ // Big environment meshes like walls/floor/ceiling cover huge screen areas and
+ // otherwise steal the hit from small objects such as the office computer.
+ float score = centerScreen.z;
+ score += centerDistance * (mouseInsideBounds ? 0.02f : 1.0f);
+ score += Math.Min(projectedArea, 750000f) * 0.004f;
+
+ if (!mouseInsideBounds)
+ score += 5000f;
+
+ bool environment = IsEnvironmentObject(go);
+ bool useful = LooksLikeUsefulVisibleObject(go);
+
+ if (environment)
+ score += 20000f;
+
+ if (useful)
+ score -= 4000f;
+
+ if (score < bestAnyScore)
+ {
+ bestAnyScore = score;
+ bestAnyObject = go;
+ }
+
+ if (!environment && score < bestPropScore)
+ {
+ bestPropScore = score;
+ bestPropObject = go;
+ }
+ }
+
+ // Prefer non-environment renderers when present. This prevents a wall/floor collider
+ // behind a desk object from stealing the selection.
+ if (bestPropObject != null && bestPropObject)
+ {
+ result = PromoteToUsefulParent(bestPropObject);
+ return true;
+ }
+
+ if (bestAnyObject != null && bestAnyObject)
+ {
+ result = PromoteToUsefulParent(bestAnyObject);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ ExplorerCore.LogWarning($"WorldInspector renderer fallback failed: {ex.Message}");
}
+
+ return false;
}
- internal void OnHitGameObject(GameObject obj)
+ private static bool ShouldPreferRendererCandidate(GameObject physicsObject, GameObject rendererObject)
+ {
+ try
+ {
+ if (!physicsObject || !rendererObject)
+ return false;
+
+ if (physicsObject == rendererObject)
+ return false;
+
+ bool physicsIsEnvironment = IsEnvironmentObject(physicsObject);
+ bool rendererIsEnvironment = IsEnvironmentObject(rendererObject);
+
+ if (physicsIsEnvironment && !rendererIsEnvironment)
+ return true;
+
+ if (LooksLikeUsefulVisibleObject(rendererObject) && !LooksLikeUsefulVisibleObject(physicsObject))
+ return true;
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ private static bool IsEnvironmentObject(GameObject go)
+ {
+ try
+ {
+ if (!go)
+ return false;
+
+ string name = go.name ?? string.Empty;
+ string lower = name.ToLowerInvariant();
+
+ if (lower.Contains("wall") ||
+ lower.Contains("floor") ||
+ lower.Contains("ceiling") ||
+ lower.Contains("roof") ||
+ lower.Contains("static"))
+ {
+ return true;
+ }
+
+ Transform? current = go.transform.parent;
+ int depth = 0;
+ while (current != null && depth < 4)
+ {
+ string parentName = current.gameObject.name ?? string.Empty;
+ string parentLower = parentName.ToLowerInvariant();
+
+ if (parentLower.Contains("walls") ||
+ parentLower.Contains("floors") ||
+ parentLower.Contains("ceiling") ||
+ parentLower.Contains("office/walls"))
+ {
+ return true;
+ }
+
+ current = current.parent;
+ depth++;
+ }
+ }
+ catch
+ {
+ }
+
+ return false;
+ }
+
+ private static bool LooksLikeUsefulVisibleObject(GameObject go)
+ {
+ try
+ {
+ if (!go)
+ return false;
+
+ if (HasNamedComponent(go, "Interact") ||
+ HasNamedComponent(go, "UsableObject") ||
+ HasNamedComponent(go, "RayLookAt") ||
+ HasNamedComponent(go, "Rack") ||
+ HasNamedComponent(go, "RackMount") ||
+ HasNamedComponent(go, "Server") ||
+ HasNamedComponent(go, "NetworkSwitch") ||
+ HasNamedComponent(go, "PatchPanel") ||
+ HasNamedComponent(go, "Computer") ||
+ HasNamedComponent(go, "Monitor"))
+ {
+ return true;
+ }
+
+ string lower = (go.name ?? string.Empty).ToLowerInvariant();
+ return lower.Contains("computer") ||
+ lower.Contains("monitor") ||
+ lower.Contains("screen") ||
+ lower.Contains("keyboard") ||
+ lower.Contains("mouse") ||
+ lower.Contains("desk") ||
+ lower.Contains("table") ||
+ lower.Contains("chair") ||
+ lower.Contains("rack") ||
+ lower.Contains("server") ||
+ lower.Contains("switch") ||
+ lower.Contains("radio") ||
+ lower.Contains("fridge") ||
+ lower.Contains("whiteboard");
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool TryGetScreenBounds(Camera camera, Bounds bounds, out Rect rect)
+ {
+ rect = default;
+
+ Vector3 min = bounds.min;
+ Vector3 max = bounds.max;
+
+ Vector3[] corners =
+ {
+ new(min.x, min.y, min.z),
+ new(min.x, min.y, max.z),
+ new(min.x, max.y, min.z),
+ new(min.x, max.y, max.z),
+ new(max.x, min.y, min.z),
+ new(max.x, min.y, max.z),
+ new(max.x, max.y, min.z),
+ new(max.x, max.y, max.z),
+ };
+
+ bool foundVisibleCorner = false;
+ float xMin = float.MaxValue;
+ float yMin = float.MaxValue;
+ float xMax = float.MinValue;
+ float yMax = float.MinValue;
+
+ foreach (Vector3 corner in corners)
+ {
+ Vector3 screen = camera.WorldToScreenPoint(corner);
+ if (screen.z <= 0f)
+ continue;
+
+ foundVisibleCorner = true;
+ xMin = Math.Min(xMin, screen.x);
+ yMin = Math.Min(yMin, screen.y);
+ xMax = Math.Max(xMax, screen.x);
+ yMax = Math.Max(yMax, screen.y);
+ }
+
+ if (!foundVisibleCorner)
+ return false;
+
+ rect = Rect.MinMaxRect(xMin, yMin, xMax, yMax);
+ return true;
+ }
+
+ internal void OnHitGameObject(GameObject obj, string source = "")
{
if (obj != lastHitObject)
{
lastHitObject = obj;
- MouseInspector.Instance.UpdateObjectNameLabel($"Click to Inspect: {obj.name}");
+
+ string sourceLabel = string.IsNullOrEmpty(source) ? string.Empty : $" ({source})";
+ MouseInspector.Instance.UpdateObjectNameLabel($"Click to Inspect:{sourceLabel} {obj.name}");
MouseInspector.Instance.UpdateObjectPathLabel($"Path: {obj.transform.GetTransformPath(true)}");
}
}
@@ -93,6 +699,16 @@ public override void OnEndInspect()
{
if (MainCamera != null)
{
+ try
+ {
+ MouseInspector.Instance.UpdateInspectorTitle(
+ $"World Inspector ({MainCamera.name}) (press ESC to cancel)"
+ );
+ }
+ catch
+ {
+ }
+
return MainCamera;
}
diff --git a/src/UI/Panels/CSConsolePanel.cs b/src/UI/Panels/CSConsolePanel.cs
index ae6359b..b85646e 100644
--- a/src/UI/Panels/CSConsolePanel.cs
+++ b/src/UI/Panels/CSConsolePanel.cs
@@ -176,17 +176,7 @@ private void ConstructSafeInputField(GameObject inputArea, int fontSize)
{
ExplorerCore.Log("Unity 6000 detected, using safe C# Console input fallback.");
- InputFallback = UIFactory.CreateInputField(inputArea, "ConsoleInput", ConsoleController.STARTUP_TEXT);
- UIFactory.SetLayoutElement(InputFallback.Component.gameObject, minWidth: 100, minHeight: 30, flexibleWidth: 5000, flexibleHeight: 5000);
-
- InputFallback.Component.lineType = InputField.LineType.MultiLineNewline;
- InputFallback.Component.targetGraphic.color = new Color(0.12f, 0.12f, 0.12f);
- InputFallback.Component.textComponent.alignment = TextAnchor.UpperLeft;
- InputFallback.Component.textComponent.fontSize = fontSize;
- InputFallback.Component.textComponent.horizontalOverflow = HorizontalWrapMode.Wrap;
- InputFallback.PlaceholderText.alignment = TextAnchor.UpperLeft;
- InputFallback.PlaceholderText.fontSize = fontSize;
- InputFallback.PlaceholderText.horizontalOverflow = HorizontalWrapMode.Wrap;
+ InputFallback = Unity6000SafeUI.CreateSafeMultiLineInput(inputArea, "ConsoleInput", ConsoleController.STARTUP_TEXT, fontSize);
}
private void ConfigureInputVisuals(int fontSize)
diff --git a/src/UI/Panels/OptionsPanel.cs b/src/UI/Panels/OptionsPanel.cs
index 6e0cb99..3c4e9dc 100644
--- a/src/UI/Panels/OptionsPanel.cs
+++ b/src/UI/Panels/OptionsPanel.cs
@@ -19,6 +19,24 @@ public class OptionsPanel : UEPanel, ICacheObjectController, ICellPoolDataSource
public override bool ShouldSaveActiveState => false;
public override bool ShowByDefault => false;
+ private static readonly KeyCode[] MouseInspectKeybindChoices =
+ {
+ KeyCode.None,
+ KeyCode.F8,
+ KeyCode.F9,
+ KeyCode.F10,
+ KeyCode.F11,
+ KeyCode.Insert,
+ KeyCode.Home,
+ KeyCode.End,
+ KeyCode.PageUp,
+ KeyCode.PageDown,
+ KeyCode.Mouse2,
+ KeyCode.Mouse3,
+ KeyCode.Mouse4,
+ KeyCode.Mouse5
+ };
+
// Entry holders
private readonly List configEntries = new();
@@ -77,6 +95,8 @@ protected override void ConstructPanelContent()
UIFactory.SetLayoutElement(saveBtn.Component.gameObject, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 0);
saveBtn.OnClick += ConfigManager.Handler.SaveConfig;
+ CreateMouseInspectKeybindDropdowns();
+
// Config entries
ScrollPool scrollPool = UIFactory.CreateScrollPool(
@@ -87,5 +107,131 @@ protected override void ConstructPanelContent()
scrollPool.Initialize(this);
}
+
+ private void CreateMouseInspectKeybindDropdowns()
+ {
+ GameObject block = UIFactory.CreateVerticalGroup(
+ ContentRoot,
+ "DataCenterMouseInspectKeybindBlock",
+ true,
+ false,
+ true,
+ true,
+ 4,
+ new Vector4(6, 6, 6, 6),
+ new Color(0.08f, 0.08f, 0.08f));
+
+ UIFactory.SetLayoutElement(block, minHeight: 110, flexibleHeight: 0, flexibleWidth: 9999);
+
+ Text header = UIFactory.CreateLabel(
+ block,
+ "DataCenterMouseInspectKeybindHeader",
+ "Data Center Mouse Inspect Keybinds",
+ TextAnchor.MiddleLeft);
+
+ UIFactory.SetLayoutElement(header.gameObject, minHeight: 22, flexibleWidth: 9999, flexibleHeight: 0);
+
+ Text hint = UIFactory.CreateLabel(
+ block,
+ "DataCenterMouseInspectKeybindHint",
+ "Use these dropdowns if the normal KeyCode field does not accept input. Recommended: World = F8, UI = F9.",
+ TextAnchor.MiddleLeft);
+
+ hint.fontSize = 12;
+ hint.color = new Color(0.75f, 0.75f, 0.75f);
+ UIFactory.SetLayoutElement(hint.gameObject, minHeight: 24, flexibleWidth: 9999, flexibleHeight: 0);
+
+ CreateMouseInspectKeybindRow(
+ block,
+ "World Inspect",
+ ConfigManager.World_MouseInspect_Keybind,
+ KeyCode.F8);
+
+ CreateMouseInspectKeybindRow(
+ block,
+ "UI Inspect",
+ ConfigManager.UI_MouseInspect_Keybind,
+ KeyCode.F9);
+ }
+
+ private void CreateMouseInspectKeybindRow(
+ GameObject parent,
+ string label,
+ ConfigElement config,
+ KeyCode recommendedKey)
+ {
+ GameObject row = UIFactory.CreateHorizontalGroup(
+ parent,
+ label.Replace(" ", "") + "KeybindRow",
+ 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,
+ label.Replace(" ", "") + "KeybindLabel",
+ label,
+ TextAnchor.MiddleLeft);
+
+ UIFactory.SetLayoutElement(labelText.gameObject, minWidth: 120, minHeight: 24, flexibleWidth: 0, flexibleHeight: 0);
+
+ GameObject dropdownObject = UIFactory.CreateDropdown(
+ row,
+ label.Replace(" ", "") + "KeybindDropdown",
+ out Dropdown dropdown,
+ config.Value.ToString(),
+ 14,
+ index =>
+ {
+ if (index < 0 || index >= MouseInspectKeybindChoices.Length)
+ return;
+
+ KeyCode selected = MouseInspectKeybindChoices[index];
+ config.Value = selected;
+
+ ExplorerCore.Log(label + " keybind set to " + selected);
+
+ try
+ {
+ ConfigManager.Handler.SaveConfig();
+ }
+ catch (Exception ex)
+ {
+ ExplorerCore.LogWarning("Failed to save " + label + " keybind: " + ex.Message);
+ }
+ });
+
+ dropdown.options.Clear();
+
+ foreach (KeyCode key in MouseInspectKeybindChoices)
+ {
+ string text = key == recommendedKey
+ ? key + " (recommended)"
+ : key.ToString();
+
+ dropdown.options.Add(new Dropdown.OptionData(text));
+ }
+
+ dropdown.value = GetMouseInspectKeyIndex(config.Value);
+ dropdown.RefreshShownValue();
+
+ UIFactory.SetLayoutElement(dropdownObject, minWidth: 220, minHeight: 24, flexibleWidth: 9999, flexibleHeight: 0);
+ }
+
+ private static int GetMouseInspectKeyIndex(KeyCode key)
+ {
+ for (int i = 0; i < MouseInspectKeybindChoices.Length; i++)
+ {
+ if (MouseInspectKeybindChoices[i] == key)
+ return i;
+ }
+
+ return 0;
+ }
}
}
diff --git a/src/UI/Unity6000SafeUI.cs b/src/UI/Unity6000SafeUI.cs
new file mode 100644
index 0000000..1442138
--- /dev/null
+++ b/src/UI/Unity6000SafeUI.cs
@@ -0,0 +1,45 @@
+using UniverseLib.UI;
+using UniverseLib.UI.Models;
+
+namespace UnityExplorer.UI;
+
+internal static class Unity6000SafeUI
+{
+ private static readonly Color SafeInputColor = new(0.12f, 0.12f, 0.12f);
+ private static readonly Color SafeContainerColor = new(0.1f, 0.1f, 0.1f);
+
+ public static InputFieldRef CreateSafeMultiLineInput(GameObject parent, string name, string placeholder, int fontSize)
+ {
+ InputFieldRef input = UIFactory.CreateInputField(parent, name, placeholder);
+ UIFactory.SetLayoutElement(input.Component.gameObject, minWidth: 100, minHeight: 30, flexibleWidth: 5000, flexibleHeight: 5000);
+
+ input.Component.lineType = InputField.LineType.MultiLineNewline;
+ input.Component.targetGraphic.color = SafeInputColor;
+ input.Component.textComponent.alignment = TextAnchor.UpperLeft;
+ input.Component.textComponent.fontSize = fontSize;
+ input.Component.textComponent.horizontalOverflow = HorizontalWrapMode.Wrap;
+ input.PlaceholderText.alignment = TextAnchor.UpperLeft;
+ input.PlaceholderText.fontSize = fontSize;
+ input.PlaceholderText.horizontalOverflow = HorizontalWrapMode.Wrap;
+
+ return input;
+ }
+
+ public static GameObject CreateSafeVerticalContainer(GameObject parent, string name, Vector4 padding)
+ {
+ GameObject container = UIFactory.CreateVerticalGroup(
+ parent,
+ name,
+ true,
+ false,
+ true,
+ true,
+ 0,
+ padding,
+ SafeContainerColor,
+ TextAnchor.UpperLeft);
+
+ UIFactory.SetLayoutElement(container, flexibleWidth: 9999, flexibleHeight: 9999);
+ return container;
+ }
+}
diff --git a/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs b/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs
index cf0999e..41033ab 100644
--- a/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs
+++ b/src/UI/Widgets/EvaluateWidget/GenericConstructorWidget.cs
@@ -121,19 +121,7 @@ internal void ConstructUI(GameObject parent)
{
ExplorerCore.Log("Unity 6000 detected, using safe Generic Arguments container fallback.");
- ArgsHolder = UIFactory.CreateVerticalGroup(
- UIRoot,
- "GenericArgsContent",
- true,
- false,
- true,
- true,
- 0,
- new Vector4(5, 5, 5, 5),
- new(0.1f, 0.1f, 0.1f),
- TextAnchor.UpperLeft);
-
- UIFactory.SetLayoutElement(ArgsHolder, flexibleWidth: 9999, flexibleHeight: 9999);
+ ArgsHolder = Unity6000SafeUI.CreateSafeVerticalContainer(UIRoot, "GenericArgsContent", new Vector4(5, 5, 5, 5));
}
else
{