mirror of
https://github.com/sinai-dev/UnityExplorer.git
synced 2025-06-23 00:52:31 +08:00
Compare commits
409 Commits
Author | SHA1 | Date | |
---|---|---|---|
a80cef4c1d | |||
450bb77f2e | |||
0479102db6 | |||
93181f02be | |||
371054d6df | |||
427f23b80a | |||
a0d5ab8792 | |||
297034e38b | |||
57f59d1295 | |||
fbdb84eefa | |||
6989ea1b19 | |||
fcdfeb2dec | |||
a1d0b6432e | |||
0b84405e57 | |||
c31e0949d3 | |||
5f0495a7ea | |||
28de6779d8 | |||
fae85c2968 | |||
fc8fa9aa7a | |||
bcf9a801a9 | |||
20298aa47b | |||
e2c1c186c3 | |||
9ce6508828 | |||
506e75c5fe | |||
a2f22051f0 | |||
4e3203a91b | |||
a411ce2dba | |||
48132b3d46 | |||
e49ed3028f | |||
31dd54d25c | |||
3cfdd6fa43 | |||
1fa1283a68 | |||
afa4135b67 | |||
48d1cf574d | |||
df0abbc847 | |||
5c9dcb1d43 | |||
602770d980 | |||
f26371f95f | |||
a673c39f4a | |||
40583cae3d | |||
20018a9ba9 | |||
dabf92a1a5 | |||
b7e275f02c | |||
f393e0d706 | |||
ab8acc9e84 | |||
66c30ee70e | |||
ddd271c00d | |||
30fe9e4dde | |||
fa3a436037 | |||
ca27d2b20f | |||
4315e0c547 | |||
3e19e74329 | |||
5a3ffebadc | |||
6e6b6239d8 | |||
03c8e5a8bd | |||
44f7209843 | |||
734e45cf9f | |||
97838e0b3a | |||
304b47f898 | |||
3d1fcbcd9f | |||
36fc17aa43 | |||
adfa29e63c | |||
3ffdcea73b | |||
dfd55260a8 | |||
29c78dc5a6 | |||
bf59d9d6cd | |||
bb0c59534a | |||
802bb722bc | |||
9ca992b0d7 | |||
5f3b3a6870 | |||
f5bce439cb | |||
e2b2c9038a | |||
fbefccd6b7 | |||
271c91f0d0 | |||
eb221bd868 | |||
5b516eb4cc | |||
601567f9d2 | |||
7ff508b874 | |||
09a7cd35cf | |||
db4a338d26 | |||
c08e02057c | |||
362fcdc51a | |||
73bd172e4d | |||
454d3bd0b4 | |||
f815a13d9a | |||
65c4d49274 | |||
d99137526e | |||
92447b55cd | |||
87d5d5a2de | |||
08cff3386b | |||
6033200579 | |||
94ec1c4908 | |||
7a400e762c | |||
67f9f744bb | |||
7a59f9a2a1 | |||
9b42eef1b9 | |||
830000b019 | |||
34910ab273 | |||
86b036095e | |||
b57e5be2e6 | |||
2d8ae45814 | |||
66dc262a68 | |||
4342901206 | |||
58b7c72a5c | |||
623dc7b7be | |||
e6f4939cc9 | |||
7a539ba78b | |||
0d10f94eb5 | |||
91671bf243 | |||
a72877befb | |||
16335c1bc4 | |||
8fab9e6268 | |||
fdd9039cca | |||
dcca980635 | |||
bfcab8248e | |||
97c20144f1 | |||
aaab10a0a0 | |||
b42a8dbe6a | |||
d7008db22e | |||
8c1913fe80 | |||
8d8c9ac7c9 | |||
f35beeaf58 | |||
d150ff3455 | |||
c4fa0d6bcd | |||
a5f56cf5a3 | |||
8c822b2ee9 | |||
4681b7e192 | |||
97093733d8 | |||
615f77979f | |||
ba3cf970d9 | |||
b2fb571b18 | |||
67ce6f946a | |||
1b82ccb49f | |||
480eb5afd5 | |||
88ea2a09c9 | |||
a678aa4d78 | |||
eaf478e314 | |||
d391968b32 | |||
70349ad7c7 | |||
78f2d1070f | |||
51307563ab | |||
185d1aaa0f | |||
3d6e8fcbf8 | |||
1daf4fade4 | |||
d92fb3f83f | |||
fe24b68fe2 | |||
6bf92b9a96 | |||
9f1cab019d | |||
a131404ac7 | |||
773900d749 | |||
342fc6bdb8 | |||
e85ea6ac3a | |||
af889e64cb | |||
0274022ce4 | |||
211576e0f8 | |||
3e42d7479f | |||
df4dea20c1 | |||
ece0c43067 | |||
6311c8d09a | |||
04739d0be8 | |||
a46acba265 | |||
5515b2eae4 | |||
9992029e28 | |||
14105785f0 | |||
365269b0dd | |||
0b973393d1 | |||
701d4431ae | |||
bfa73bcb55 | |||
b0bbeb3cf8 | |||
1a26623080 | |||
041f2938f7 | |||
9e7bb1a625 | |||
36f23b7cdc | |||
b51b743df4 | |||
805aff07cc | |||
bcdaf3b97e | |||
cb8e947fdf | |||
c8899be3ae | |||
cd5c69c965 | |||
5427312f18 | |||
eb7e80d910 | |||
a54888ae3a | |||
4f0553d293 | |||
9f0f7f9b57 | |||
383c6f19e8 | |||
428fab28f9 | |||
760b2981ad | |||
eee7d6bcc4 | |||
e270f205a1 | |||
084aee617c | |||
d0e508727a | |||
a9a53ba924 | |||
3cd9819790 | |||
5abfa3da67 | |||
e5d2d29a47 | |||
6a47e542e5 | |||
f1b83e7c9e | |||
44c6503ae2 | |||
6970dcbbc7 | |||
ac9c2d5286 | |||
496a5de55e | |||
b062924af7 | |||
5aef8ddc99 | |||
c134c1752e | |||
e4615aff84 | |||
8c5640dd12 | |||
76f71d9f3d | |||
82e9c08ae6 | |||
019e589947 | |||
d7b0fff949 | |||
7dbf694642 | |||
2fc9657560 | |||
fb6e413153 | |||
ca65affb5c | |||
f4e473f8e6 | |||
8c5e7678a6 | |||
32ad61baea | |||
df330420a3 | |||
5af9d3104d | |||
6977f1a31c | |||
26fb53f183 | |||
cccd02255f | |||
83f15c7168 | |||
bf8f838f01 | |||
a915e1028f | |||
8b1379f17e | |||
0c7d8f8435 | |||
72c3af3dd7 | |||
54f78ac10b | |||
2a9c4972dd | |||
e93edc5b19 | |||
c25abfe3ff | |||
8c3603baa0 | |||
021db69409 | |||
1c216c0d86 | |||
12fe19ba8e | |||
89022db5fc | |||
ccd08c3a63 | |||
843d7ed451 | |||
3e44317861 | |||
70d66f93a5 | |||
275225a284 | |||
b61020fe67 | |||
ada239c828 | |||
fedecb80a0 | |||
97738b00c1 | |||
59cbeec103 | |||
a9f6ed8729 | |||
7241247d05 | |||
ec215a0006 | |||
6e9bb83099 | |||
712bf7b669 | |||
b4b96134c4 | |||
d3b942065d | |||
9ea50e232c | |||
4128f8519d | |||
53e1e6bf22 | |||
b67ac145d4 | |||
586c63b668 | |||
65323e3b16 | |||
cfeb5ba018 | |||
8a15c11289 | |||
4019af5936 | |||
34c8ad3646 | |||
617d68f7e9 | |||
2efce9eb0e | |||
18d2518231 | |||
cef4c2f3fb | |||
1d24af5666 | |||
4f50afdddc | |||
caad39bb9a | |||
06122fe8c9 | |||
e6b253fed9 | |||
7b700cbe55 | |||
c04a864b74 | |||
c828d9b642 | |||
26052621e5 | |||
d101e7e35c | |||
57aace26d3 | |||
3d94b51d40 | |||
d34aeb81b3 | |||
d8f532d913 | |||
4931117b1e | |||
1f996f52fe | |||
00c28f781a | |||
f080379e8a | |||
2977fd4df5 | |||
56875e0641 | |||
8534c08f49 | |||
1ee10c2507 | |||
9e8a18a5e1 | |||
6c7acf7690 | |||
e70a1e96da | |||
22435176bf | |||
e4ff86259b | |||
961ff80c6d | |||
a89d66cf81 | |||
302ff29e36 | |||
8d9d8f76c2 | |||
3dc458ab1b | |||
f5c0b339ae | |||
ad61ff243a | |||
bfaba56b76 | |||
1c5306b7c8 | |||
15ec64b106 | |||
ab8b736f7e | |||
ea1e183c4a | |||
d6cde68a44 | |||
d76bc1f812 | |||
74ff1d8f01 | |||
2378925a8b | |||
8080129d58 | |||
4b8298fd2e | |||
ad055b4383 | |||
23483a6108 | |||
0bc14b2f76 | |||
dba9bdbdc2 | |||
a2a2b09d33 | |||
324d3afa5b | |||
99e11b41a3 | |||
b0d54b1d80 | |||
a2ff37e36d | |||
07ddba3c3d | |||
73cde0f91f | |||
af3ee07143 | |||
9f8d53f55a | |||
5a0c2390ce | |||
f3cd84804d | |||
fda5afae46 | |||
7f6a4514e4 | |||
1487372832 | |||
6d4cc66079 | |||
0cf8309a82 | |||
38bd19c243 | |||
bda286ddae | |||
5f2f3fe1c6 | |||
59156492e7 | |||
ebb89b1b8b | |||
30f847dc23 | |||
7ffaf62895 | |||
f509a985e7 | |||
eb58ab5327 | |||
feb86a77fd | |||
012994ed02 | |||
212d9a4d5e | |||
a4f774b6b2 | |||
bd6de84f93 | |||
0d385c9cb8 | |||
fdfadcefc1 | |||
fc26452f64 | |||
31fa786574 | |||
29b453dc91 | |||
837d5792be | |||
1a8c2499fa | |||
e1e40950f8 | |||
edbb9a2882 | |||
0a9639f8a9 | |||
b32675e3b1 | |||
ff7c822d69 | |||
a619df8e01 | |||
300b35c2d3 | |||
7a253fa0a0 | |||
8b5e385c28 | |||
bcc89455a7 | |||
2e5fb72716 | |||
876cffd864 | |||
7cb4faa596 | |||
c8f3a7f430 | |||
6cd7029ffc | |||
225a07bc1b | |||
085c79441b | |||
40d32e1919 | |||
480a8cb31c | |||
9bdcccaaa1 | |||
0f69833283 | |||
1769a4ed8d | |||
c8a64c39b1 | |||
c1d3aab8e3 | |||
9a9048bcd8 | |||
dec113d2ee | |||
b03349a3e0 | |||
896da0157d | |||
a58e2a0fad | |||
b13aa74fa1 | |||
8ef6df043c | |||
f89455549e | |||
a6ff9e02e2 | |||
51f5c68598 | |||
5bb612cb5a | |||
a3fcac1acb | |||
d1d7572945 | |||
7eb4b1bc77 | |||
a6c24f91e4 | |||
9e4c335a05 | |||
a1c2dfbe50 | |||
a5a07a0a23 | |||
e0fd682c81 | |||
7426bd1dd6 | |||
b39b044f79 | |||
7a2b4aa257 | |||
3762d14bdb | |||
3628f3db31 | |||
d39fea69c3 | |||
95e8b3aa58 | |||
b68145385c | |||
2310f2f7ce | |||
2cc403ad17 | |||
c2d9b9b59e | |||
c748be7bcc |
60
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
name: Bug Report
|
||||
description: File a bug or crash report
|
||||
title: "[Bug]: "
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for submitting a bug report, please fill out as much detail as possible.
|
||||
- type: checkboxes
|
||||
id: latestversion
|
||||
attributes:
|
||||
label: Are you on the latest version of UnityExplorer?
|
||||
description: If not, you must update first.
|
||||
options:
|
||||
- label: Yes, I'm on the latest version of UnityExplorer.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Which release are you using?
|
||||
description: Please select your environment for UnityExplorer.
|
||||
options:
|
||||
- BepInEx IL2CPP
|
||||
- BepInEx 6.X Mono
|
||||
- BepInEx 5.X Mono
|
||||
- MelonLoader IL2CPP
|
||||
- MelonLoader Mono
|
||||
- Standalone IL2CPP
|
||||
- Standalone Mono
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: game
|
||||
attributes:
|
||||
label: Which game did this occur on?
|
||||
description: Please tell us the name of the game. If it's a personal or private project, just let us know the Unity version.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Describe the issue.
|
||||
description: What happened? Should something else have happened instead? Please provide steps to reproduce the issue if possible.
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please copy and paste any relevant logs and stack traces.
|
||||
* Unity log: `%userprofile%\AppData\LocalLow\{Company}\{Game}\Player.log` or `output_log.txt`
|
||||
* BepInEx: `BepInEx\LogOutput.log`
|
||||
* MelonLoader: `MelonLoader\latest.log`
|
||||
* Standalone: `{DLL_Location}\UnityExplorer\Logs\` (pick the most recent one)
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
18
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
name: New feature or enhancement
|
||||
description: Suggest or discuss a feature or enhancement for UnityExplorer
|
||||
title: "[Enhancement]: "
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to discuss UnityExplorer, please provide as much detail as possible.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the new feature or enhancement
|
||||
description: |
|
||||
Please go into as much detail as necessary in describing the new feature or enhancement.
|
||||
If providing examples or suggestions for the required C# code, please use syntax-highlighted code blocks.
|
||||
validations:
|
||||
required: true
|
13
.github/ISSUE_TEMPLATE/other.yaml
vendored
Normal file
13
.github/ISSUE_TEMPLATE/other.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Other
|
||||
description: Something else?
|
||||
title: "[Other]: "
|
||||
labels: [Other]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: |
|
||||
Please describe the issue in as much detail as possible.
|
||||
validations:
|
||||
required: true
|
97
.github/workflows/dotnet.yml
vendored
Normal file
97
.github/workflows/dotnet.yml
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
name: Build UnityExplorer
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
if: "!contains(github.event.head_commit.message, '-noci')"
|
||||
|
||||
steps:
|
||||
# Checkout latest with submodules
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# Setup tools
|
||||
- name: Setup msbuild
|
||||
uses: microsoft/setup-msbuild@v1
|
||||
|
||||
- name: Setup nuget
|
||||
uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NuGetAPIKey }}
|
||||
nuget-version: '5.x'
|
||||
|
||||
# Build Il2CppAssemblyUnhollower
|
||||
- run: msbuild lib\Il2CppAssemblyUnhollower\UnhollowerBaseLib\UnhollowerBaseLib.csproj -t:Restore -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release
|
||||
|
||||
# Build mcs
|
||||
- run: nuget restore lib\mcs-unity\mcs.sln
|
||||
- run: msbuild lib\mcs-unity\mcs\mcs.csproj -t:Restore -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release
|
||||
|
||||
# Build UnityExplorer releases, and upload artifacts
|
||||
|
||||
- run: nuget restore src\UnityExplorer.sln
|
||||
|
||||
# BepInEx Il2Cpp
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_BIE_Cpp
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.BepInEx.Il2Cpp
|
||||
path: ./Release/UnityExplorer.BepInEx.Il2Cpp/*
|
||||
|
||||
# BepInEx 5 Mono
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_BIE5_Mono
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.BepInEx5.Mono
|
||||
path: ./Release/UnityExplorer.BepInEx5.Mono/*
|
||||
|
||||
# BepInEx 6 Mono
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_BIE6_Mono
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.BepInEx6.Mono
|
||||
path: ./Release/UnityExplorer.BepInEx6.Mono/*
|
||||
|
||||
# MelonLoader Il2Cpp
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_ML_Cpp
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.MelonLoader.Il2Cpp
|
||||
path: ./Release/UnityExplorer.MelonLoader.Il2Cpp/*
|
||||
|
||||
# MelonLoader Mono
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_ML_Mono
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.MelonLoader.Mono
|
||||
path: ./Release/UnityExplorer.MelonLoader.Mono/*
|
||||
|
||||
# Standalone Il2Cpp
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_STANDALONE_Cpp
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.Standalone.Il2Cpp
|
||||
path: ./Release/UnityExplorer.Standalone.Il2Cpp/*
|
||||
|
||||
# Standalone Mono
|
||||
- run: msbuild src\UnityExplorer.csproj -t:Rebuild -p:Platform="AnyCPU" -p:Configuration=Release_STANDALONE_Mono
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: UnityExplorer.Standalone.Mono
|
||||
path: ./Release/UnityExplorer.Standalone.Mono/*
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,7 +9,6 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*/mcs-unity*
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
[submodule "lib/Il2CppAssemblyUnhollower"]
|
||||
path = lib/Il2CppAssemblyUnhollower
|
||||
url = https://github.com/knah/Il2CppAssemblyUnhollower
|
||||
[submodule "lib/mcs-unity"]
|
||||
path = lib/mcs-unity
|
||||
url = https://github.com/sinai-dev/mcs-unity
|
168
README.md
168
README.md
@ -3,72 +3,52 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
An in-game explorer and a suite of debugging tools for <a href="https://docs.unity3d.com/Manual/IL2CPP.html">IL2CPP</a> and <b>Mono</b> Unity games, to aid with modding development.
|
||||
🔍 An in-game UI for exploring, debugging and modifying Unity games.
|
||||
</p>
|
||||
<p align="center">
|
||||
✔️ Supports most Unity versions from 5.2 to 2021+ (IL2CPP and Mono).
|
||||
</p>
|
||||
|
||||
## Releases [](../../releases/latest) [](../../releases) [](../../releases/latest)
|
||||
# Releases [](../../releases)
|
||||
|
||||
| Mod Loader | IL2CPP | Mono |
|
||||
| ----------- | ------ | ---- |
|
||||
| [BepInEx](https://github.com/BepInEx/BepInEx) 6.X | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx6.Mono.zip) |
|
||||
| [BepInEx](https://github.com/BepInEx/BepInEx) 5.X | ✖️ n/a | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx5.Mono.zip) |
|
||||
| [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3 | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Mono.zip) |
|
||||
| Standalone | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Mono.zip) |
|
||||
[](../../releases/latest) [](https://github.com/sinai-dev/UnityExplorer/actions) [](../../releases/latest)
|
||||
|
||||
## How to install
|
||||
⚡ Thunderstore releases: [BepInEx Mono](https://thunderstore.io/package/sinai-dev/UnityExplorer) | [BepInEx IL2CPP](https://gtfo.thunderstore.io/package/sinai-dev/UnityExplorer_IL2CPP) | [MelonLoader IL2CPP](https://boneworks.thunderstore.io/package/sinai-dev/UnityExplorer_IL2CPP_ML)
|
||||
|
||||
### BepInEx
|
||||
## BepInEx
|
||||
|
||||
1. Install [BepInEx](https://github.com/BepInEx/BepInEx) for your game. For IL2CPP you should use [BepInEx 6 (Bleeding Edge)](https://builds.bepis.io/projects/bepinex_be), for Mono you should use [BepInEx 5](https://github.com/BepInEx/BepInEx/releases) (until Mono support stabilizes in BepInEx 6).
|
||||
2. Download the UnityExplorer release for BepInEx IL2CPP or Mono above.
|
||||
3. Take the `UnityExplorer.BIE.___.dll` file and put it in `[GameFolder]\BepInEx\plugins\`
|
||||
4. In IL2CPP, you will need to download the [Unity libs](https://github.com/LavaGang/Unity-Runtime-Libraries) for the game's Unity version and put them in the `BepInEx\unity-libs\` folder.
|
||||
| Release | IL2CPP | Mono |
|
||||
| ------- | ------ | ---- |
|
||||
| BIE 6.X | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx6.Mono.zip) |
|
||||
| BIE 5.X | ✖️ n/a | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.BepInEx5.Mono.zip) |
|
||||
|
||||
### MelonLoader
|
||||
1. Take the `UnityExplorer.BIE.[version].dll` file and put it in `BepInEx\plugins\`
|
||||
2. In IL2CPP, you will need to download the [Unity libs](https://github.com/LavaGang/Unity-Runtime-Libraries) for the game's Unity version, create a folder `BepInEx\unity-libs\`, then extract the Unity libs into this folder.
|
||||
|
||||
1. Install [MelonLoader](https://github.com/HerpDerpinstine/MelonLoader) 0.3+ for your game. Version 0.3 is currently in pre-release, so you must "Enable ALPHA Releases" in your MelonLoader Installer settings to see the option for it.
|
||||
2. Download the UnityExplorer release for MelonLoader IL2CPP or Mono above.
|
||||
3. Take the `UnityExplorer.ML.___.dll` file and put it in the `[GameFolder]\Mods\` folder.
|
||||
<i>Note: BepInEx 6 is obtainable via [BepisBuilds](https://builds.bepis.io/projects/bepinex_be)</i>
|
||||
|
||||
### Standalone
|
||||
## MelonLoader
|
||||
|
||||
The standalone release requires you to also load `0Harmony.dll` (HarmonyX, from the `lib\` folder) to function properly, and the IL2CPP also requires `UnhollowerBaseLib.dll` as well. The Mono release should be fairly easy to use with any loader, but the IL2CPP one may be tricky, I'd recommend just using BepInEx or MelonLoader for IL2CPP.
|
||||
| Release | IL2CPP | Mono |
|
||||
| ------- | ------ | ---- |
|
||||
| ML 0.4+ | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.MelonLoader.Mono.zip) |
|
||||
|
||||
1. Load the UnityExplorer DLL from your mod or inject it, as well as `0Harmony.dll` and `UnhollowerBaseLib.dll` as required.
|
||||
2. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();`
|
||||
3. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish.
|
||||
1. Take the `UnityExplorer.ML.[version].dll` file and put it in the `Mods\` folder created by MelonLoader.
|
||||
|
||||
## Issues and contributions
|
||||
## Standalone
|
||||
|
||||
Both issue reports and PR contributions are welcome in this repository.
|
||||
| IL2CPP | Mono |
|
||||
| ------ | ---- |
|
||||
| ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Il2Cpp.zip) | ✅ [link](https://github.com/sinai-dev/UnityExplorer/releases/latest/download/UnityExplorer.Standalone.Mono.zip) |
|
||||
|
||||
### Issue reporting
|
||||
The standalone release can be used with any injector or loader of your choice, but it requires you to load the dependencies manually: HarmonyX, and the IL2CPP version also requires that you set up an [Il2CppAssemblyUnhollower runtime](https://github.com/knah/Il2CppAssemblyUnhollower#required-external-setup).
|
||||
|
||||
To report an issue with UnityExplorer, please use the following template (as well as any other information / images you can provide).
|
||||
1. Load the required libs - HarmonyX, and Il2CppAssemblyUnhollower if IL2CPP
|
||||
2. Load the UnityExplorer DLL
|
||||
3. Create an instance of Unity Explorer with `UnityExplorer.ExplorerStandalone.CreateInstance();`
|
||||
4. Optionally subscribe to the `ExplorerStandalone.OnLog` event to handle logging if you wish
|
||||
|
||||
Template:
|
||||
|
||||
```
|
||||
* Game issue occurs on:
|
||||
* UnityExplorer version: (eg BIE.IL2CPP v3.3.3, etc)
|
||||
|
||||
* Description of issue:
|
||||
|
||||
* Unity log link:
|
||||
* Mod Loader log link:
|
||||
```
|
||||
|
||||
Please upload your Unity log file to [Pastebin](https://pastebin.com/) (or equivalent service) and provide a link to the paste.
|
||||
|
||||
* The log should be found at `%userprofile%\AppData\LocalLow\[Company]\[Game]\` unless redirected by your Mod Loader.
|
||||
* The file will be called either `output_log.txt` or `Player.log`
|
||||
|
||||
As well as the Unity log, please upload your Mod Loader's log:
|
||||
|
||||
* BepInEx: `BepInEx\LogOutput.log`
|
||||
* MelonLoader: `MelonLoader\Latest.log`
|
||||
|
||||
## Features
|
||||
# Features
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/sinai-dev/UnityExplorer/master/img/preview.png">
|
||||
@ -76,69 +56,73 @@ As well as the Unity log, please upload your Mod Loader's log:
|
||||
</a>
|
||||
</p>
|
||||
|
||||
* <b>Scene Explorer</b>: Simple menu to traverse the Transform heirarchy of the scene.
|
||||
* <b>GameObject Inspector</b>: Various helpful tools to see and manipulate the GameObject, similar to what you can do in the Editor.
|
||||
* <b>Reflection Inspector</b>: Inspect Properties and Fields. Can also set primitive values and evaluate primitive methods.
|
||||
* <b>Search</b>: Search for UnityEngine.Objects with various filters, or use the helpers for static Instances and Classes.
|
||||
* <b>C# Console</b>: Interactive console for evaluating C# methods on the fly, with some basic helpers.
|
||||
* <b>Inspect-under-mouse</b>: Hover over an object with a collider and inspect it by clicking on it. There's also a UI mode to inspect UI objects.
|
||||
### Object Explorer
|
||||
|
||||
### C# Console Tips
|
||||
* Use the <b>Scene Explorer</b> tab to traverse the active scenes, as well as the DontDestroyOnLoad and HideAndDontSave objects.
|
||||
* The "HideAndDontSave" scene contains objects with that flag, as well as Assets and Resources which are not in any scene but behave the same way.
|
||||
* You can use the Scene Loader to easily load any of the scenes in the build (may not work for Unity 5.X games)
|
||||
* Use the <b>Object Search</b> tab to search for Unity objects (including GameObjects, Components, etc), C# Singletons or Static Classes.
|
||||
* Use the UnityObject search to look for any objects which derive from `UnityEngine.Object`, with optional filters
|
||||
* The singleton search will look for any classes with a typical "Instance" field, and check it for a current value. This may cause unexpected behaviour in some IL2CPP games as we cannot distinguish between true properties and field-properties, so some property accessors will be invoked.
|
||||
|
||||
The C# Console can be used to define temporary classes and methods, or it can be used to evaluate an expression, but you cannot do both at the same time.
|
||||
### Inspector
|
||||
|
||||
For example, you could run this code to define a temporary class (it will be visible within the console until you run `Reset();`).
|
||||
The inspector is used to see detailed information on objects of any type and manipulate their values, as well as to inspect C# Classes with static reflection.
|
||||
|
||||
```csharp
|
||||
public class MyClass
|
||||
{
|
||||
public static void Method()
|
||||
{
|
||||
UnityExplorer.ExplorerCore.Log("hello");
|
||||
}
|
||||
}
|
||||
```
|
||||
* The <b>GameObject Inspector</b> (tab prefix `[G]`) is used to inspect a `GameObject`, and to see and manipulate its Transform and Components.
|
||||
* You can edit any of the input fields in the inspector (excluding readonly fields) and press <b>Enter</b> to apply your changes. You can also do this to the GameObject path as a way to change the GameObject's parent. Press the <b>Escape</b> key to cancel your edits.
|
||||
* <i>note: When inspecting a GameObject with a Canvas, the transform controls may be overridden by the RectTransform anchors.</i>
|
||||
* The <b>Reflection Inspectors</b> (tab prefix `[R]` and `[S]`) are used for everything else
|
||||
* Automatic updating is not enabled by default, and you must press Apply for any changes you make to take effect.
|
||||
* Press the `▼` button to expand certain values such as strings, enums, lists, dictionaries, some structs, etc
|
||||
* Use the filters at the top to quickly find the members you are looking for
|
||||
* For `Texture2D` objects, there is a `View Texture` button at the top of the inspector which lets you view it and save it as a PNG file. Currently there are no other similar helpers yet, but I may add more at some point for Mesh, Sprite, Material, etc
|
||||
|
||||
You could then delete or comment out the class and run the following expression to run that method:
|
||||
### C# Console
|
||||
|
||||
```csharp
|
||||
MyClass.Method();
|
||||
```
|
||||
* The C# Console uses the `Mono.CSharp.Evaluator` to define temporary classes or run immediate REPL code.
|
||||
* You can execute a script automatically on startup by naming it `startup.cs` and placing it in the `UnityExplorer\Scripts\` folder (this folder will be created where you placed the DLL file).
|
||||
* See the "Help" dropdown in the C# console menu for more detailed information.
|
||||
|
||||
However, you cannot define a class and run it both at the same time. You must either define class(es) and run that, or define an expression and run that.
|
||||
### Hook Manager
|
||||
|
||||
You can also make use of the helper methods in the console to simplify some tasks, which you can see listed when the console has nothing entered for input. These methods are **not** accessible within any temporary classes you define, they can only be used in the expression context.
|
||||
* The Hooks panel allows you to hook methods at the click of a button for debugging purposes.
|
||||
* Simply enter any class (generic types not yet supported) and hook the methods you want from the menu.
|
||||
* You can edit the source code of the generated hook with the "Edit Hook Source" button. Accepted method names are `Prefix` (which can return `bool` or `void`), `Postfix`, `Finalizer` (which can return `Exception` or `void`), and `Transpiler` (which must return `IEnumerable<HarmonyLib.CodeInstruction>`). You can define multiple patches if you wish.
|
||||
|
||||
### Logging
|
||||
### Mouse-Inspect
|
||||
|
||||
Explorer saves all logs to disk (only keeps the most recent 10 logs). They can be found in a "UnityExplorer" folder in the same place as where you put the DLL file.
|
||||
|
||||
These logs are also visible in the Debug Console part of the UI.
|
||||
* The "Mouse Inspect" dropdown on the main UnityExplorer navbar allows you to inspect objects under the mouse.
|
||||
* <b>World</b>: uses Physics.Raycast to look for Colliders
|
||||
* <b>UI</b>: uses GraphicRaycasters to find UI objects
|
||||
|
||||
### Settings
|
||||
|
||||
You can change the settings via the "Options" page of the main menu, or directly from the config file.
|
||||
* You can change the settings via the "Options" tab of the menu, or directly from the config file.
|
||||
* BepInEx: `BepInEx\config\com.sinai.unityexplorer.cfg`
|
||||
* MelonLoader: `UserData\MelonPreferences.cfg`
|
||||
* Standalone `{DLL_location}\UnityExplorer\config.ini`
|
||||
|
||||
Depending on the release you are using, the config file will be found at:
|
||||
* BepInEx: `BepInEx\config\com.sinai.unityexplorer.cfg`
|
||||
* MelonLoader: `UserData\MelonPreferences.cfg`
|
||||
* Standalone `{DLL_location}\UnityExplorer\config.ini`
|
||||
# Building
|
||||
|
||||
## Building
|
||||
For Visual Studio:
|
||||
|
||||
Building the project should be straight-forward, the references are all inside the `lib\` folder.
|
||||
0. Clone the repository and run `git submodule update --init --recursive` to get the submodules.
|
||||
1. Open the `src\UnityExplorer.sln` project.
|
||||
2. Build `mcs` (Release/AnyCPU, you may need to run `nuget restore mcs.sln`), and if using IL2CPP then build `Il2CppAssemblyUnhollower` (Release/AnyCPU) as well.
|
||||
3. Build the UnityExplorer release(s) you want to use, either by selecting the config as the Active Config, or batch-building.
|
||||
|
||||
1. Open the `src\UnityExplorer.sln` project in Visual Studio.
|
||||
2. Select `Solution 'UnityExplorer' (1 of 1 project)` in the Solution Explorer panel, and set the <b>Active config</b> property to the version you want to build, then build it. Alternatively, use "Batch Build" and select all releases.
|
||||
3. The DLLs are built to the `Release\` folder in the root of the repository.
|
||||
4. If ILRepack complains about an error, just change the Active config to a different release and then back again. This sometimes happens for the first time you build the project.
|
||||
If you fork the repository on GitHub you can build using the [dotnet workflow](https://github.com/sinai-dev/UnityExplorer/blob/master/.github/workflows/dotnet.yml):
|
||||
|
||||
## Acknowledgments
|
||||
0. Click on the Actions tab and enable workflows in your repository
|
||||
1. Click on the "Build UnityExplorer" workflow, then click "Run Workflow" and run it manually, or make a new commit to trigger the workflow.
|
||||
2. Take the artifact from the completed run.
|
||||
|
||||
* [ManlyMarco](https://github.com/ManlyMarco) for [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor) \[[license](THIRDPARTY_LICENSES.md#runtimeunityeditor-license)\], snippets from the REPL Console were used for UnityExplorer's C# Console.
|
||||
# Acknowledgments
|
||||
|
||||
* [ManlyMarco](https://github.com/ManlyMarco) for [Runtime Unity Editor](https://github.com/ManlyMarco/RuntimeUnityEditor) \[[license](THIRDPARTY_LICENSES.md#runtimeunityeditor-license)\], the ScriptEvaluator from RUE's REPL console was used as the base for UnityExplorer's C# console.
|
||||
* [denikson](https://github.com/denikson) (aka Horse) for [mcs-unity](https://github.com/denikson/mcs-unity) \[no license\], used as the `Mono.CSharp` reference for the C# Console.
|
||||
* [HerpDerpenstine](https://github.com/HerpDerpinstine) for [MelonCoroutines](https://github.com/LavaGang/MelonLoader/blob/6cc958ec23b5e2e8453a73bc2e0d5aa353d4f0d1/MelonLoader.Support.Il2Cpp/MelonCoroutines.cs) \[[license](THIRDPARTY_LICENSES.md#melonloader-license)\], they were included for standalone IL2CPP coroutine support.
|
||||
* [InGameCodeEditor](https://assetstore.unity.com/packages/tools/gui/ingame-code-editor-144254) \[[license](THIRDPARTY_LICENSES.md#ingamecodeeditor-license)\] was used as the base for the syntax highlighting for UnityExplorer's C# console (`UnityExplorer.UI.Main.CSConsole.Lexer`).
|
||||
|
||||
### Disclaimer
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
* [RuntimeUnityEditor License](#runtimeunityeditor-license)
|
||||
* [MelonLoader License](#melonloader-license)
|
||||
* [InGameCodeEditor License](#ingamecodeeditor-license)
|
||||
|
||||
## RuntimeUnityEditor License
|
||||
|
||||
@ -873,195 +872,3 @@ Public License instead of this License. But first, please read
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## InGameCodeEditor License
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2020 - 2021 Lava Gang
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
BIN
img/preview.png
BIN
img/preview.png
Binary file not shown.
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 495 KiB |
BIN
img/social.png
BIN
img/social.png
Binary file not shown.
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 232 KiB |
BIN
lib/0Harmony.dll
BIN
lib/0Harmony.dll
Binary file not shown.
BIN
lib/BepInEx.5/BepInEx.dll
Normal file
BIN
lib/BepInEx.5/BepInEx.dll
Normal file
Binary file not shown.
BIN
lib/BepInEx.6.IL2CPP/BepInEx.Core.dll
Normal file
BIN
lib/BepInEx.6.IL2CPP/BepInEx.Core.dll
Normal file
Binary file not shown.
BIN
lib/BepInEx.6.IL2CPP/BepInEx.IL2CPP.dll
Normal file
BIN
lib/BepInEx.6.IL2CPP/BepInEx.IL2CPP.dll
Normal file
Binary file not shown.
BIN
lib/BepInEx.6.Mono/BepInEx.Core.dll
Normal file
BIN
lib/BepInEx.6.Mono/BepInEx.Core.dll
Normal file
Binary file not shown.
BIN
lib/BepInEx.6.Mono/BepInEx.Unity.dll
Normal file
BIN
lib/BepInEx.6.Mono/BepInEx.Unity.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/BepInEx.dll
BIN
lib/BepInEx.dll
Binary file not shown.
Binary file not shown.
1
lib/Il2CppAssemblyUnhollower
Submodule
1
lib/Il2CppAssemblyUnhollower
Submodule
Submodule lib/Il2CppAssemblyUnhollower added at 0099c25069
Binary file not shown.
BIN
lib/MelonLoader/MelonLoader.dll
Normal file
BIN
lib/MelonLoader/MelonLoader.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
lib/mcs-unity
Submodule
1
lib/mcs-unity
Submodule
Submodule lib/mcs-unity added at 0bc7359dd7
BIN
lib/mcs.dll
BIN
lib/mcs.dll
Binary file not shown.
BIN
lib/mono/UnityEngine.UI.dll
Normal file
BIN
lib/mono/UnityEngine.UI.dll
Normal file
Binary file not shown.
BIN
lib/mono/UnityEngine.dll
Normal file
BIN
lib/mono/UnityEngine.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
137
src/CSConsole/CSAutoCompleter.cs
Normal file
137
src/CSConsole/CSAutoCompleter.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.CSConsole.Lexers;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets.AutoComplete;
|
||||
|
||||
namespace UnityExplorer.CSConsole
|
||||
{
|
||||
public class CSAutoCompleter : ISuggestionProvider
|
||||
{
|
||||
public InputFieldRef InputField => ConsoleController.Input;
|
||||
|
||||
public bool AnchorToCaretPosition => true;
|
||||
|
||||
bool ISuggestionProvider.AllowNavigation => true;
|
||||
|
||||
public void OnSuggestionClicked(Suggestion suggestion)
|
||||
{
|
||||
ConsoleController.InsertSuggestionAtCaret(suggestion.UnderlyingValue);
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(this);
|
||||
}
|
||||
|
||||
private readonly HashSet<char> delimiters = new HashSet<char>
|
||||
{
|
||||
'{', '}', ',', ';', '<', '>', '(', ')', '[', ']', '=', '|', '&', '?'
|
||||
};
|
||||
|
||||
private readonly List<Suggestion> suggestions = new List<Suggestion>();
|
||||
|
||||
public void CheckAutocompletes()
|
||||
{
|
||||
if (string.IsNullOrEmpty(InputField.Text))
|
||||
{
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(this);
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.Clear();
|
||||
|
||||
int caret = Math.Max(0, Math.Min(InputField.Text.Length - 1, InputField.Component.caretPosition - 1));
|
||||
int startIdx = caret;
|
||||
|
||||
// If the character at the caret index is whitespace or delimiter,
|
||||
// or if the next character (if it exists) is not whitespace,
|
||||
// then we don't want to provide suggestions.
|
||||
if (char.IsWhiteSpace(InputField.Text[caret])
|
||||
|| delimiters.Contains(InputField.Text[caret])
|
||||
|| (InputField.Text.Length > caret + 1 && !char.IsWhiteSpace(InputField.Text[caret + 1])))
|
||||
{
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// get the current composition string (from caret back to last delimiter)
|
||||
while (startIdx > 0)
|
||||
{
|
||||
startIdx--;
|
||||
char c = InputField.Text[startIdx];
|
||||
if (delimiters.Contains(c) || char.IsWhiteSpace(c))
|
||||
{
|
||||
startIdx++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
string input = InputField.Text.Substring(startIdx, caret - startIdx + 1);
|
||||
|
||||
// Get MCS completions
|
||||
|
||||
string[] evaluatorCompletions = ConsoleController.Evaluator.GetCompletions(input, out string prefix);
|
||||
|
||||
if (evaluatorCompletions != null && evaluatorCompletions.Any())
|
||||
{
|
||||
suggestions.AddRange(from completion in evaluatorCompletions
|
||||
select new Suggestion(GetHighlightString(prefix, completion), completion));
|
||||
}
|
||||
|
||||
// Get manual namespace completions
|
||||
|
||||
foreach (var ns in ReflectionUtility.AllNamespaces)
|
||||
{
|
||||
if (ns.StartsWith(input))
|
||||
{
|
||||
if (!namespaceHighlights.ContainsKey(ns))
|
||||
namespaceHighlights.Add(ns, $"<color=#CCCCCC>{ns}</color>");
|
||||
|
||||
string completion = ns.Substring(input.Length, ns.Length - input.Length);
|
||||
suggestions.Add(new Suggestion(namespaceHighlights[ns], completion));
|
||||
}
|
||||
}
|
||||
|
||||
// Get manual keyword completions
|
||||
|
||||
foreach (var kw in KeywordLexer.keywords)
|
||||
{
|
||||
if (kw.StartsWith(input))// && kw.Length > input.Length)
|
||||
{
|
||||
if (!keywordHighlights.ContainsKey(kw))
|
||||
keywordHighlights.Add(kw, $"<color=#{SignatureHighlighter.keywordBlueHex}>{kw}</color>");
|
||||
|
||||
string completion = kw.Substring(input.Length, kw.Length - input.Length);
|
||||
suggestions.Add(new Suggestion(keywordHighlights[kw], completion));
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestions.Any())
|
||||
{
|
||||
AutoCompleteModal.Instance.TakeOwnership(this);
|
||||
AutoCompleteModal.Instance.SetSuggestions(suggestions);
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly Dictionary<string, string> namespaceHighlights = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, string> keywordHighlights = new Dictionary<string, string>();
|
||||
|
||||
private readonly StringBuilder highlightBuilder = new StringBuilder();
|
||||
private const string OPEN_HIGHLIGHT = "<color=cyan>";
|
||||
|
||||
private string GetHighlightString(string prefix, string completion)
|
||||
{
|
||||
highlightBuilder.Clear();
|
||||
highlightBuilder.Append(OPEN_HIGHLIGHT);
|
||||
highlightBuilder.Append(prefix);
|
||||
highlightBuilder.Append(SignatureHighlighter.CLOSE_COLOR);
|
||||
highlightBuilder.Append(completion);
|
||||
return highlightBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
692
src/CSConsole/ConsoleController.cs
Normal file
692
src/CSConsole/ConsoleController.cs
Normal file
@ -0,0 +1,692 @@
|
||||
using Mono.CSharp;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Input;
|
||||
using UnityExplorer.CSConsole;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UnityExplorer.UI.Widgets.AutoComplete;
|
||||
|
||||
namespace UnityExplorer.CSConsole
|
||||
{
|
||||
public static class ConsoleController
|
||||
{
|
||||
public static ScriptEvaluator Evaluator;
|
||||
public static LexerBuilder Lexer;
|
||||
public static CSAutoCompleter Completer;
|
||||
|
||||
private static HashSet<string> usingDirectives;
|
||||
private static StringBuilder evaluatorOutput;
|
||||
|
||||
public static CSConsolePanel Panel => UIManager.GetPanel<CSConsolePanel>(UIManager.Panels.CSConsole);
|
||||
public static InputFieldRef Input => Panel.Input;
|
||||
|
||||
public static int LastCaretPosition { get; private set; }
|
||||
internal static float defaultInputFieldAlpha;
|
||||
|
||||
// Todo save as config?
|
||||
public static bool EnableCtrlRShortcut { get; private set; } = true;
|
||||
public static bool EnableAutoIndent { get; private set; } = true;
|
||||
public static bool EnableSuggestions { get; private set; } = true;
|
||||
|
||||
internal static string ScriptsFolder => Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Scripts");
|
||||
|
||||
internal static readonly string[] DefaultUsing = new string[]
|
||||
{
|
||||
"System",
|
||||
"System.Linq",
|
||||
"System.Text",
|
||||
"System.Collections.Generic",
|
||||
"UnityEngine",
|
||||
#if CPP
|
||||
"UnhollowerBaseLib",
|
||||
"UnhollowerRuntimeLib",
|
||||
#endif
|
||||
};
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
// Make sure console is supported on this platform
|
||||
try
|
||||
{
|
||||
ResetConsole(false);
|
||||
// ensure the compiler is supported (if this fails then SRE is probably stubbed)
|
||||
Evaluator.Compile("0 == 0");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DisableConsole(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup console
|
||||
Lexer = new LexerBuilder();
|
||||
Completer = new CSAutoCompleter();
|
||||
|
||||
SetupHelpInteraction();
|
||||
|
||||
Panel.OnInputChanged += OnInputChanged;
|
||||
Panel.InputScroller.OnScroll += OnInputScrolled;
|
||||
Panel.OnCompileClicked += Evaluate;
|
||||
Panel.OnResetClicked += ResetConsole;
|
||||
Panel.OnHelpDropdownChanged += HelpSelected;
|
||||
Panel.OnAutoIndentToggled += OnToggleAutoIndent;
|
||||
Panel.OnCtrlRToggled += OnToggleCtrlRShortcut;
|
||||
Panel.OnSuggestionsToggled += OnToggleSuggestions;
|
||||
Panel.OnPanelResized += OnInputScrolled;
|
||||
|
||||
// Run startup script
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(ScriptsFolder))
|
||||
Directory.CreateDirectory(ScriptsFolder);
|
||||
|
||||
var startupPath = Path.Combine(ScriptsFolder, "startup.cs");
|
||||
if (File.Exists(startupPath))
|
||||
{
|
||||
ExplorerCore.Log($"Executing startup script from '{startupPath}'...");
|
||||
var text = File.ReadAllText(startupPath);
|
||||
Input.Text = text;
|
||||
Evaluate();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception executing startup script: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region UI Listeners and options
|
||||
|
||||
// TODO save
|
||||
|
||||
private static void OnToggleAutoIndent(bool value)
|
||||
{
|
||||
EnableAutoIndent = value;
|
||||
}
|
||||
|
||||
private static void OnToggleCtrlRShortcut(bool value)
|
||||
{
|
||||
EnableCtrlRShortcut = value;
|
||||
}
|
||||
|
||||
private static void OnToggleSuggestions(bool value)
|
||||
{
|
||||
EnableSuggestions = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Evaluating
|
||||
|
||||
public static void ResetConsole() => ResetConsole(true);
|
||||
|
||||
public static void ResetConsole(bool logSuccess = true)
|
||||
{
|
||||
if (SRENotSupported)
|
||||
return;
|
||||
|
||||
if (Evaluator != null)
|
||||
Evaluator.Dispose();
|
||||
|
||||
evaluatorOutput = new StringBuilder();
|
||||
Evaluator = new ScriptEvaluator(new StringWriter(evaluatorOutput))
|
||||
{
|
||||
InteractiveBaseClass = typeof(ScriptInteraction)
|
||||
};
|
||||
|
||||
usingDirectives = new HashSet<string>();
|
||||
foreach (var use in DefaultUsing)
|
||||
AddUsing(use);
|
||||
|
||||
if (logSuccess)
|
||||
ExplorerCore.Log($"C# Console reset. Using directives:\r\n{Evaluator.GetUsing()}");
|
||||
}
|
||||
|
||||
public static void AddUsing(string assemblyName)
|
||||
{
|
||||
if (!usingDirectives.Contains(assemblyName))
|
||||
{
|
||||
Evaluate($"using {assemblyName};", true);
|
||||
usingDirectives.Add(assemblyName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Evaluate()
|
||||
{
|
||||
if (SRENotSupported)
|
||||
return;
|
||||
|
||||
Evaluate(Input.Text);
|
||||
}
|
||||
|
||||
public static void Evaluate(string input, bool supressLog = false)
|
||||
{
|
||||
if (SRENotSupported)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Compile the code. If it returned a CompiledMethod, it is REPL.
|
||||
CompiledMethod repl = Evaluator.Compile(input);
|
||||
|
||||
if (repl != null)
|
||||
{
|
||||
// Valid REPL, we have a delegate to the evaluation.
|
||||
try
|
||||
{
|
||||
object ret = null;
|
||||
repl.Invoke(ref ret);
|
||||
var result = ret?.ToString();
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
ExplorerCore.Log($"Invoked REPL, result: {ret}");
|
||||
else
|
||||
ExplorerCore.Log($"Invoked REPL (no return value)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception invoking REPL: {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The compiled code was not REPL, so it was a using directive or it defined classes.
|
||||
|
||||
string output = Evaluator._textWriter.ToString();
|
||||
var outputSplit = output.Split('\n');
|
||||
if (outputSplit.Length >= 2)
|
||||
output = outputSplit[outputSplit.Length - 2];
|
||||
evaluatorOutput.Clear();
|
||||
|
||||
if (ScriptEvaluator._reportPrinter.ErrorsCount > 0)
|
||||
throw new FormatException($"Unable to compile the code. Evaluator's last output was:\r\n{output}");
|
||||
else if (!supressLog)
|
||||
ExplorerCore.Log($"Code compiled without errors.");
|
||||
}
|
||||
}
|
||||
catch (FormatException fex)
|
||||
{
|
||||
if (!supressLog)
|
||||
ExplorerCore.LogWarning(fex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!supressLog)
|
||||
ExplorerCore.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
// Updating and event listeners
|
||||
|
||||
private static bool settingCaretCoroutine;
|
||||
|
||||
private static void OnInputScrolled() => HighlightVisibleInput();
|
||||
|
||||
private static string previousInput;
|
||||
|
||||
// Invoked at most once per frame
|
||||
private static void OnInputChanged(string value)
|
||||
{
|
||||
if (SRENotSupported)
|
||||
return;
|
||||
|
||||
// prevent escape wiping input
|
||||
if (InputManager.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
Input.Text = previousInput;
|
||||
|
||||
if (EnableSuggestions && AutoCompleteModal.CheckEscape(Completer))
|
||||
OnAutocompleteEscaped();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
previousInput = value;
|
||||
|
||||
if (EnableSuggestions && AutoCompleteModal.CheckEnter(Completer))
|
||||
OnAutocompleteEnter();
|
||||
|
||||
if (!settingCaretCoroutine)
|
||||
{
|
||||
if (EnableAutoIndent)
|
||||
DoAutoIndent();
|
||||
}
|
||||
|
||||
var inStringOrComment = HighlightVisibleInput();
|
||||
|
||||
if (!settingCaretCoroutine)
|
||||
{
|
||||
if (EnableSuggestions)
|
||||
{
|
||||
if (inStringOrComment)
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
|
||||
else
|
||||
Completer.CheckAutocompletes();
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCaret(out _);
|
||||
}
|
||||
|
||||
private static float timeOfLastCtrlR;
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (SRENotSupported)
|
||||
return;
|
||||
|
||||
UpdateCaret(out bool caretMoved);
|
||||
|
||||
if (!settingCaretCoroutine && EnableSuggestions)
|
||||
{
|
||||
if (AutoCompleteModal.CheckEscape(Completer))
|
||||
{
|
||||
OnAutocompleteEscaped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (caretMoved)
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
|
||||
}
|
||||
|
||||
if (EnableCtrlRShortcut
|
||||
&& (InputManager.GetKey(KeyCode.LeftControl) || InputManager.GetKey(KeyCode.RightControl))
|
||||
&& InputManager.GetKeyDown(KeyCode.R)
|
||||
&& timeOfLastCtrlR.OccuredEarlierThanDefault())
|
||||
{
|
||||
timeOfLastCtrlR = Time.realtimeSinceStartup;
|
||||
Evaluate(Panel.Input.Text);
|
||||
}
|
||||
}
|
||||
|
||||
private const int CSCONSOLE_LINEHEIGHT = 18;
|
||||
|
||||
private static void UpdateCaret(out bool caretMoved)
|
||||
{
|
||||
int prevCaret = LastCaretPosition;
|
||||
caretMoved = false;
|
||||
|
||||
// Override up/down arrow movement when autocompleting
|
||||
if (EnableSuggestions && AutoCompleteModal.CheckNavigation(Completer))
|
||||
{
|
||||
Input.Component.caretPosition = LastCaretPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.Component.isFocused)
|
||||
{
|
||||
LastCaretPosition = Input.Component.caretPosition;
|
||||
caretMoved = LastCaretPosition != prevCaret;
|
||||
}
|
||||
|
||||
if (Input.Text.Length == 0)
|
||||
return;
|
||||
|
||||
// If caret moved, ensure caret is visible in the viewport
|
||||
if (caretMoved)
|
||||
{
|
||||
var charInfo = Input.TextGenerator.characters[LastCaretPosition];
|
||||
var charTop = charInfo.cursorPos.y;
|
||||
var charBot = charTop - CSCONSOLE_LINEHEIGHT;
|
||||
|
||||
var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f);
|
||||
var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height;
|
||||
|
||||
float diff = 0f;
|
||||
if (charTop > viewportMin)
|
||||
diff = charTop - viewportMin;
|
||||
else if (charBot < viewportMax)
|
||||
diff = charBot - viewportMax;
|
||||
|
||||
if (Math.Abs(diff) > 1)
|
||||
{
|
||||
var rect = Input.Rect;
|
||||
rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y - diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCaretPosition(int caretPosition)
|
||||
{
|
||||
settingCaretCoroutine = true;
|
||||
Input.Component.readOnly = true;
|
||||
RuntimeProvider.Instance.StartCoroutine(SetCaretCoroutine(caretPosition));
|
||||
}
|
||||
|
||||
internal static PropertyInfo SelectionGuardProperty => selectionGuardPropInfo ?? GetSelectionGuardPropInfo();
|
||||
|
||||
private static PropertyInfo GetSelectionGuardPropInfo()
|
||||
{
|
||||
selectionGuardPropInfo = typeof(EventSystem).GetProperty("m_SelectionGuard");
|
||||
if (selectionGuardPropInfo == null)
|
||||
selectionGuardPropInfo = typeof(EventSystem).GetProperty("m_selectionGuard");
|
||||
return selectionGuardPropInfo;
|
||||
}
|
||||
|
||||
private static PropertyInfo selectionGuardPropInfo;
|
||||
|
||||
private static IEnumerator SetCaretCoroutine(int caretPosition)
|
||||
{
|
||||
var color = Input.Component.selectionColor;
|
||||
color.a = 0f;
|
||||
Input.Component.selectionColor = color;
|
||||
try { EventSystem.current.SetSelectedGameObject(null, null); } catch { }
|
||||
yield return null;
|
||||
|
||||
try { SelectionGuardProperty.SetValue(EventSystem.current, false, null); } catch { }
|
||||
try { EventSystem.current.SetSelectedGameObject(Input.UIRoot, null); } catch { }
|
||||
Input.Component.Select();
|
||||
yield return null;
|
||||
|
||||
Input.Component.caretPosition = caretPosition;
|
||||
Input.Component.selectionFocusPosition = caretPosition;
|
||||
LastCaretPosition = Input.Component.caretPosition;
|
||||
|
||||
color.a = defaultInputFieldAlpha;
|
||||
Input.Component.selectionColor = color;
|
||||
|
||||
Input.Component.readOnly = false;
|
||||
settingCaretCoroutine = false;
|
||||
}
|
||||
|
||||
#region Lexer Highlighting
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if caret is inside string or comment, false otherwise
|
||||
/// </summary>
|
||||
private static bool HighlightVisibleInput()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Input.Text))
|
||||
{
|
||||
Panel.HighlightText.text = "";
|
||||
Panel.LineNumberText.text = "1";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate the visible lines
|
||||
|
||||
int topLine = -1;
|
||||
int bottomLine = -1;
|
||||
|
||||
// the top and bottom position of the viewport in relation to the text height
|
||||
// they need the half-height adjustment to normalize against the 'line.topY' value.
|
||||
var viewportMin = Input.Rect.rect.height - Input.Rect.anchoredPosition.y - (Input.Rect.rect.height * 0.5f);
|
||||
var viewportMax = viewportMin - Panel.InputScroller.ViewportRect.rect.height;
|
||||
|
||||
for (int i = 0; i < Input.TextGenerator.lineCount; i++)
|
||||
{
|
||||
var line = Input.TextGenerator.lines[i];
|
||||
// if not set the top line yet, and top of line is below the viewport top
|
||||
if (topLine == -1 && line.topY <= viewportMin)
|
||||
topLine = i;
|
||||
// if bottom of line is below the viewport bottom
|
||||
if ((line.topY - line.height) >= viewportMax)
|
||||
bottomLine = i;
|
||||
}
|
||||
|
||||
topLine = Math.Max(0, topLine - 1);
|
||||
bottomLine = Math.Min(Input.TextGenerator.lineCount - 1, bottomLine + 1);
|
||||
|
||||
int startIdx = Input.TextGenerator.lines[topLine].startCharIdx;
|
||||
int endIdx = (bottomLine >= Input.TextGenerator.lineCount - 1)
|
||||
? Input.Text.Length - 1
|
||||
: (Input.TextGenerator.lines[bottomLine + 1].startCharIdx - 1);
|
||||
|
||||
|
||||
// Highlight the visible text with the LexerBuilder
|
||||
|
||||
Panel.HighlightText.text = Lexer.BuildHighlightedString(Input.Text, startIdx, endIdx, topLine, LastCaretPosition, out bool ret);
|
||||
|
||||
// Set the line numbers
|
||||
|
||||
// determine true starting line number (not the same as the cached TextGenerator line numbers)
|
||||
int realStartLine = 0;
|
||||
for (int i = 0; i < startIdx; i++)
|
||||
{
|
||||
if (LexerBuilder.IsNewLine(Input.Text[i]))
|
||||
realStartLine++;
|
||||
}
|
||||
realStartLine++;
|
||||
char lastPrev = '\n';
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// append leading new lines for spacing (no point rendering line numbers we cant see)
|
||||
for (int i = 0; i < topLine; i++)
|
||||
sb.Append('\n');
|
||||
|
||||
// append the displayed line numbers
|
||||
for (int i = topLine; i <= bottomLine; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
lastPrev = Input.Text[Input.TextGenerator.lines[i].startCharIdx - 1];
|
||||
|
||||
// previous line ended with a newline character, this is an actual new line.
|
||||
if (LexerBuilder.IsNewLine(lastPrev))
|
||||
{
|
||||
sb.Append(realStartLine.ToString());
|
||||
realStartLine++;
|
||||
}
|
||||
|
||||
sb.Append('\n');
|
||||
}
|
||||
|
||||
Panel.LineNumberText.text = sb.ToString();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Autocompletes
|
||||
|
||||
public static void InsertSuggestionAtCaret(string suggestion)
|
||||
{
|
||||
settingCaretCoroutine = true;
|
||||
Input.Text = Input.Text.Insert(LastCaretPosition, suggestion);
|
||||
|
||||
SetCaretPosition(LastCaretPosition + suggestion.Length);
|
||||
LastCaretPosition = Input.Component.caretPosition;
|
||||
}
|
||||
|
||||
private static void OnAutocompleteEnter()
|
||||
{
|
||||
// Remove the new line
|
||||
int lastIdx = Input.Component.caretPosition - 1;
|
||||
Input.Text = Input.Text.Remove(lastIdx, 1);
|
||||
|
||||
// Use the selected suggestion
|
||||
Input.Component.caretPosition = LastCaretPosition;
|
||||
Completer.OnSuggestionClicked(AutoCompleteModal.SelectedSuggestion);
|
||||
}
|
||||
|
||||
private static void OnAutocompleteEscaped()
|
||||
{
|
||||
AutoCompleteModal.Instance.ReleaseOwnership(Completer);
|
||||
SetCaretPosition(LastCaretPosition);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Auto indenting
|
||||
|
||||
private static int prevContentLen = 0;
|
||||
|
||||
private static void DoAutoIndent()
|
||||
{
|
||||
if (Input.Text.Length > prevContentLen)
|
||||
{
|
||||
int inc = Input.Text.Length - prevContentLen;
|
||||
|
||||
if (inc == 1)
|
||||
{
|
||||
int caret = Input.Component.caretPosition;
|
||||
Input.Text = Lexer.IndentCharacter(Input.Text, ref caret);
|
||||
Input.Component.caretPosition = caret;
|
||||
LastCaretPosition = caret;
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo indenting for copy+pasted content
|
||||
|
||||
//ExplorerCore.Log("Content increased by " + inc);
|
||||
//var comp = Input.Text.Substring(PreviousCaretPosition, inc);
|
||||
//ExplorerCore.Log("composition string: " + comp);
|
||||
}
|
||||
}
|
||||
|
||||
prevContentLen = Input.Text.Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region "Help" interaction
|
||||
|
||||
private static bool SRENotSupported;
|
||||
|
||||
private static void DisableConsole(Exception ex)
|
||||
{
|
||||
SRENotSupported = true;
|
||||
Input.Component.readOnly = true;
|
||||
Input.Component.textComponent.color = "5d8556".ToColor();
|
||||
|
||||
if (ex is NotSupportedException)
|
||||
{
|
||||
Input.Text = $@"The C# Console has been disabled because System.Reflection.Emit threw an exception: {ex.ReflectionExToString()}
|
||||
|
||||
If the game was built with Unity's stubbed netstandard 2.0 runtime, you can fix this with UnityDoorstop:
|
||||
* Download the Unity Editor version that the game uses
|
||||
* Navigate to the folder:
|
||||
- Editor\Data\PlaybackEngines\windowsstandalonesupport\Variations\mono\Managed
|
||||
- or, Editor\Data\MonoBleedingEdge\lib\mono\4.5
|
||||
* Copy the mscorlib.dll and System.Reflection.Emit DLLs from the folder
|
||||
* Make a subfolder in the folder that contains doorstop_config.ini
|
||||
* Put the DLLs inside the subfolder
|
||||
* Set the 'dllSearchPathOverride' in doorstop_config.ini to the subfolder name";
|
||||
}
|
||||
else
|
||||
{
|
||||
Input.Text = $@"The C# Console has been disabled because of an unknown error.
|
||||
{ex}";
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> helpDict = new Dictionary<string, string>();
|
||||
|
||||
public static void SetupHelpInteraction()
|
||||
{
|
||||
var drop = Panel.HelpDropdown;
|
||||
|
||||
helpDict.Add("Help", "");
|
||||
helpDict.Add("Usings", HELP_USINGS);
|
||||
helpDict.Add("REPL", HELP_REPL);
|
||||
helpDict.Add("Classes", HELP_CLASSES);
|
||||
helpDict.Add("Coroutines", HELP_COROUTINES);
|
||||
|
||||
foreach (var opt in helpDict)
|
||||
drop.options.Add(new Dropdown.OptionData(opt.Key));
|
||||
}
|
||||
|
||||
public static void HelpSelected(int index)
|
||||
{
|
||||
if (index == 0)
|
||||
return;
|
||||
|
||||
var helpText = helpDict.ElementAt(index);
|
||||
|
||||
Input.Text = helpText.Value;
|
||||
|
||||
Panel.HelpDropdown.value = 0;
|
||||
}
|
||||
|
||||
|
||||
internal const string STARTUP_TEXT = @"<color=#5d8556>// Welcome to the UnityExplorer C# Console!
|
||||
|
||||
// It is recommended to use the Log panel (or a console log window) while using this tool.
|
||||
// Use the Help dropdown to see detailed examples of how to use the console.
|
||||
|
||||
// To execute a script automatically on startup, put the script at 'UnityExplorer\Scripts\startup.cs'</color>";
|
||||
|
||||
internal const string HELP_USINGS = @"// You can add a using directive to any namespace, but you must compile for it to take effect.
|
||||
// It will remain in effect until you Reset the console.
|
||||
using UnityEngine.UI;
|
||||
|
||||
// To see your current usings, use the ""GetUsing();"" helper.
|
||||
// Note: You cannot add usings and evaluate REPL at the same time.";
|
||||
|
||||
internal const string HELP_REPL = @"/* REPL (Read-Evaluate-Print-Loop) is a way to execute code immediately.
|
||||
* REPL code cannot contain any using directives or classes.
|
||||
* The return value of the last line of your REPL will be printed to the log.
|
||||
* Variables defined in REPL will exist until you Reset the console.
|
||||
*/
|
||||
|
||||
// eg: This code would print 'Hello, World!', and then print 6 as the return value.
|
||||
Log(""Hello, world!"");
|
||||
var x = 5;
|
||||
++x;
|
||||
|
||||
/* The following helpers are available in REPL mode:
|
||||
* GetUsing(); - prints the current using directives to the console log
|
||||
* GetVars(); - prints the names and values of the REPL variables you have defined
|
||||
* GetClasses(); - prints the names and members of the classes you have defined
|
||||
* Log(obj); - prints a message to the console log
|
||||
* CurrentTarget; - System.Object, the target of the active Inspector tab
|
||||
* AllTargets; - System.Object[], the targets of all Inspector tabs
|
||||
* Inspect(obj); - inspect the object with the Inspector
|
||||
* Inspect(someType); - inspect a Type with static reflection
|
||||
* Start(enumerator); - starts the IEnumerator as a Coroutine
|
||||
* help; - the default REPL help command, contains additional helpers.
|
||||
*/";
|
||||
|
||||
internal const string HELP_CLASSES = @"// Classes you compile will exist until the application closes.
|
||||
// You can soft-overwrite a class by compiling it again with the same name. The old class will still technically exist in memory.
|
||||
|
||||
// Compiled classes can be accessed from both inside and outside this console.
|
||||
// Note: in IL2CPP, injecting these classes with ClassInjector may crash the game!
|
||||
|
||||
public class HelloWorld
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
UnityExplorer.ExplorerCore.Log(""Hello, world!"");
|
||||
}
|
||||
}
|
||||
|
||||
// In REPL, you could call the example method above with ""HelloWorld.Main();""
|
||||
// Note: The compiler does not allow you to run REPL code and define classes at the same time.
|
||||
|
||||
// In REPL, use the ""GetClasses();"" helper to see the classes you have defined since the last Reset.";
|
||||
|
||||
internal const string HELP_COROUTINES = @"// To start a Coroutine directly, use ""Start(SomeCoroutine());"" in REPL mode.
|
||||
|
||||
// To declare a coroutine, you will need to compile it separately. For example:
|
||||
public class MyCoro
|
||||
{
|
||||
public static IEnumerator Main()
|
||||
{
|
||||
yield return null;
|
||||
UnityExplorer.ExplorerCore.Log(""Hello, world after one frame!"");
|
||||
}
|
||||
}
|
||||
// To run this Coroutine in REPL, it would look like ""Start(MyCoro.Main());""";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
361
src/CSConsole/LexerBuilder.cs
Normal file
361
src/CSConsole/LexerBuilder.cs
Normal file
@ -0,0 +1,361 @@
|
||||
using Mono.CSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CSConsole.Lexers;
|
||||
|
||||
namespace UnityExplorer.CSConsole
|
||||
{
|
||||
public struct MatchInfo
|
||||
{
|
||||
public int startIndex;
|
||||
public int endIndex;
|
||||
public bool isStringOrComment;
|
||||
public bool matchToEndOfLine;
|
||||
public string htmlColorTag;
|
||||
}
|
||||
|
||||
public class LexerBuilder
|
||||
{
|
||||
#region Core and initialization
|
||||
|
||||
public const char WHITESPACE = ' ';
|
||||
public readonly HashSet<char> IndentOpenChars = new HashSet<char> { '{', '(' };
|
||||
public readonly HashSet<char> IndentCloseChars = new HashSet<char> { '}', ')' };
|
||||
|
||||
private readonly Lexer[] lexers;
|
||||
private readonly HashSet<char> delimiters = new HashSet<char>();
|
||||
|
||||
private readonly StringLexer stringLexer = new StringLexer();
|
||||
private readonly CommentLexer commentLexer = new CommentLexer();
|
||||
|
||||
public LexerBuilder()
|
||||
{
|
||||
lexers = new Lexer[]
|
||||
{
|
||||
commentLexer,
|
||||
stringLexer,
|
||||
new SymbolLexer(),
|
||||
new NumberLexer(),
|
||||
new KeywordLexer(),
|
||||
};
|
||||
|
||||
foreach (var matcher in lexers)
|
||||
{
|
||||
foreach (char c in matcher.Delimiters)
|
||||
{
|
||||
if (!delimiters.Contains(c))
|
||||
delimiters.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>The last committed index for a match or no-match. Starts at -1 for a new parse.</summary>
|
||||
public int CommittedIndex { get; private set; }
|
||||
/// <summary>The index of the character we are currently parsing, at minimum it will be CommittedIndex + 1.</summary>
|
||||
public int CurrentIndex { get; private set; }
|
||||
|
||||
/// <summary>The current character we are parsing, determined by CurrentIndex.</summary>
|
||||
public char Current => !EndOfInput ? currentInput[CurrentIndex] : WHITESPACE;
|
||||
/// <summary>The previous character (CurrentIndex - 1), or whitespace if no previous character.</summary>
|
||||
public char Previous => CurrentIndex >= 1 ? currentInput[CurrentIndex - 1] : WHITESPACE;
|
||||
|
||||
/// <summary>Returns true if CurrentIndex is >= the current input length.</summary>
|
||||
public bool EndOfInput => CurrentIndex > currentEndIdx;
|
||||
/// <summary>Returns true if EndOfInput or current character is a new line.</summary>
|
||||
public bool EndOrNewLine => EndOfInput || IsNewLine(Current);
|
||||
|
||||
public static bool IsNewLine(char c) => c == '\n' || c == '\r';
|
||||
|
||||
private string currentInput;
|
||||
private int currentStartIdx;
|
||||
private int currentEndIdx;
|
||||
|
||||
/// <summary>
|
||||
/// Parse the range of the string with the Lexer and build a RichText-highlighted representation of it.
|
||||
/// </summary>
|
||||
/// <param name="input">The entire input string which you want to parse a section (or all) of</param>
|
||||
/// <param name="startIdx">The first character you want to highlight</param>
|
||||
/// <param name="endIdx">The last character you want to highlight</param>
|
||||
/// <param name="leadingLines">The amount of leading empty lines you want before the first character in the return string.</param>
|
||||
/// <returns>A string which contains the amount of leading lines specified, as well as the rich-text highlighted section.</returns>
|
||||
public string BuildHighlightedString(string input, int startIdx, int endIdx, int leadingLines, int caretIdx, out bool caretInStringOrComment)
|
||||
{
|
||||
caretInStringOrComment = false;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || endIdx <= startIdx)
|
||||
return input;
|
||||
|
||||
currentInput = input;
|
||||
currentStartIdx = startIdx;
|
||||
currentEndIdx = endIdx;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < leadingLines; i++)
|
||||
sb.Append('\n');
|
||||
|
||||
int lastUnhighlighted = startIdx;
|
||||
foreach (var match in GetMatches())
|
||||
{
|
||||
// append non-highlighted text between last match and this
|
||||
for (int i = lastUnhighlighted; i < match.startIndex; i++)
|
||||
sb.Append(input[i]);
|
||||
|
||||
// append the highlighted match
|
||||
sb.Append(match.htmlColorTag);
|
||||
for (int i = match.startIndex; i <= match.endIndex && i <= currentEndIdx; i++)
|
||||
sb.Append(input[i]);
|
||||
sb.Append(SignatureHighlighter.CLOSE_COLOR);
|
||||
|
||||
// update the last unhighlighted start index
|
||||
lastUnhighlighted = match.endIndex + 1;
|
||||
|
||||
int matchEndIdx = match.endIndex;
|
||||
if (match.matchToEndOfLine)
|
||||
{
|
||||
while (input.Length - 1 >= matchEndIdx)
|
||||
{
|
||||
matchEndIdx++;
|
||||
if (IsNewLine(input[matchEndIdx]))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check caretIdx to determine inStringOrComment state
|
||||
if (caretIdx >= match.startIndex && (caretIdx <= (matchEndIdx+1) || (caretIdx >= input.Length && matchEndIdx >= input.Length - 1)))
|
||||
caretInStringOrComment = match.isStringOrComment;
|
||||
}
|
||||
|
||||
// Append trailing unhighlighted input
|
||||
while (lastUnhighlighted <= endIdx)
|
||||
{
|
||||
sb.Append(input[lastUnhighlighted]);
|
||||
lastUnhighlighted++;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
// Match builder, iterates through each Lexer and returns all matches found.
|
||||
|
||||
public IEnumerable<MatchInfo> GetMatches()
|
||||
{
|
||||
CommittedIndex = currentStartIdx - 1;
|
||||
Rollback();
|
||||
|
||||
while (!EndOfInput)
|
||||
{
|
||||
SkipWhitespace();
|
||||
bool anyMatch = false;
|
||||
int startIndex = CommittedIndex + 1;
|
||||
|
||||
foreach (var lexer in lexers)
|
||||
{
|
||||
if (lexer.TryMatchCurrent(this))
|
||||
{
|
||||
anyMatch = true;
|
||||
|
||||
yield return new MatchInfo
|
||||
{
|
||||
startIndex = startIndex,
|
||||
endIndex = CommittedIndex,
|
||||
htmlColorTag = lexer.ColorTag,
|
||||
isStringOrComment = lexer is StringLexer || lexer is CommentLexer,
|
||||
};
|
||||
break;
|
||||
}
|
||||
else
|
||||
Rollback();
|
||||
}
|
||||
|
||||
if (!anyMatch)
|
||||
{
|
||||
CurrentIndex = CommittedIndex + 1;
|
||||
Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Methods used by the Lexers for interfacing with the current parse process
|
||||
|
||||
public char PeekNext(int amount = 1)
|
||||
{
|
||||
CurrentIndex += amount;
|
||||
return Current;
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
CommittedIndex = Math.Min(currentEndIdx, CurrentIndex);
|
||||
}
|
||||
|
||||
public void Rollback()
|
||||
{
|
||||
CurrentIndex = CommittedIndex + 1;
|
||||
}
|
||||
|
||||
public void RollbackBy(int amount)
|
||||
{
|
||||
CurrentIndex = Math.Max(CommittedIndex + 1, CurrentIndex - amount);
|
||||
}
|
||||
|
||||
public bool IsDelimiter(char character, bool orWhitespace = false, bool orLetterOrDigit = false)
|
||||
{
|
||||
return delimiters.Contains(character)
|
||||
|| (orWhitespace && char.IsWhiteSpace(character))
|
||||
|| (orLetterOrDigit && char.IsLetterOrDigit(character));
|
||||
}
|
||||
|
||||
private void SkipWhitespace()
|
||||
{
|
||||
// peek and commit as long as there is whitespace
|
||||
while (!EndOfInput && char.IsWhiteSpace(Current))
|
||||
{
|
||||
Commit();
|
||||
PeekNext();
|
||||
}
|
||||
|
||||
if (!char.IsWhiteSpace(Current))
|
||||
Rollback();
|
||||
}
|
||||
|
||||
#region Auto Indenting
|
||||
|
||||
// Using the Lexer for indenting as it already has what we need to tokenize strings and comments.
|
||||
// At the moment this only handles when a single newline or close-delimiter is composed.
|
||||
// Does not handle copy+paste or any other characters yet.
|
||||
|
||||
public string IndentCharacter(string input, ref int caretIndex)
|
||||
{
|
||||
int lastCharIndex = caretIndex - 1;
|
||||
char c = input[lastCharIndex];
|
||||
|
||||
// we only want to indent for new lines and close indents
|
||||
if (!IsNewLine(c) && !IndentCloseChars.Contains(c))
|
||||
return input;
|
||||
|
||||
// perform a light parse up to the caret to determine indent level
|
||||
currentInput = input;
|
||||
currentStartIdx = 0;
|
||||
currentEndIdx = lastCharIndex;
|
||||
CommittedIndex = -1;
|
||||
Rollback();
|
||||
|
||||
int indent = 0;
|
||||
|
||||
while (!EndOfInput)
|
||||
{
|
||||
if (CurrentIndex >= lastCharIndex)
|
||||
{
|
||||
// reached the caret index
|
||||
if (indent <= 0)
|
||||
break;
|
||||
|
||||
if (IsNewLine(c))
|
||||
input = IndentNewLine(input, indent, ref caretIndex);
|
||||
else // closing indent
|
||||
input = IndentCloseDelimiter(input, indent, lastCharIndex, ref caretIndex);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Try match strings and comments (Lexer will commit to the end of the match)
|
||||
if (stringLexer.TryMatchCurrent(this) || commentLexer.TryMatchCurrent(this))
|
||||
{
|
||||
PeekNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Still parsing, check indent
|
||||
|
||||
if (IndentOpenChars.Contains(Current))
|
||||
indent++;
|
||||
else if (IndentCloseChars.Contains(Current))
|
||||
indent--;
|
||||
|
||||
Commit();
|
||||
PeekNext();
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private string IndentNewLine(string input, int indent, ref int caretIndex)
|
||||
{
|
||||
// continue until the end of line or next non-whitespace character.
|
||||
// if there's a close-indent on this line, reduce the indent level.
|
||||
while (CurrentIndex < input.Length - 1)
|
||||
{
|
||||
CurrentIndex++;
|
||||
char next = input[CurrentIndex];
|
||||
if (IsNewLine(next))
|
||||
break;
|
||||
if (char.IsWhiteSpace(next))
|
||||
continue;
|
||||
else if (IndentCloseChars.Contains(next))
|
||||
indent--;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (indent > 0)
|
||||
{
|
||||
input = input.Insert(caretIndex, new string('\t', indent));
|
||||
caretIndex += indent;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private string IndentCloseDelimiter(string input, int indent, int lastCharIndex, ref int caretIndex)
|
||||
{
|
||||
if (CurrentIndex > lastCharIndex)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
// lower the indent level by one as we would not have accounted for this closing symbol
|
||||
indent--;
|
||||
|
||||
// go back from the caret to the start of the line, calculate how much indent we need to adjust.
|
||||
while (CurrentIndex > 0)
|
||||
{
|
||||
CurrentIndex--;
|
||||
char prev = input[CurrentIndex];
|
||||
if (IsNewLine(prev))
|
||||
break;
|
||||
if (!char.IsWhiteSpace(prev))
|
||||
{
|
||||
// the line containing the closing bracket has non-whitespace characters before it. do not indent.
|
||||
indent = 0;
|
||||
break;
|
||||
}
|
||||
else if (prev == '\t')
|
||||
indent--;
|
||||
}
|
||||
|
||||
if (indent > 0)
|
||||
{
|
||||
input = input.Insert(caretIndex, new string('\t', indent));
|
||||
caretIndex += indent;
|
||||
}
|
||||
else if (indent < 0)
|
||||
{
|
||||
// line is overly indented
|
||||
input = input.Remove(lastCharIndex - 1, -indent);
|
||||
caretIndex += indent;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
53
src/CSConsole/Lexers/CommentLexer.cs
Normal file
53
src/CSConsole/Lexers/CommentLexer.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public class CommentLexer : Lexer
|
||||
{
|
||||
private enum CommentType
|
||||
{
|
||||
Line,
|
||||
Block
|
||||
}
|
||||
|
||||
// forest green
|
||||
protected override Color HighlightColor => new Color(0.34f, 0.65f, 0.29f, 1.0f);
|
||||
|
||||
public override bool TryMatchCurrent(LexerBuilder lexer)
|
||||
{
|
||||
if (lexer.Current == '/')
|
||||
{
|
||||
lexer.PeekNext();
|
||||
if (lexer.Current == '/')
|
||||
{
|
||||
// line comment. read to end of line or file.
|
||||
do
|
||||
{
|
||||
lexer.Commit();
|
||||
lexer.PeekNext();
|
||||
}
|
||||
while (!lexer.EndOrNewLine);
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (lexer.Current == '*')
|
||||
{
|
||||
// block comment, read until end of file or closing '*/'
|
||||
lexer.PeekNext();
|
||||
do
|
||||
{
|
||||
lexer.PeekNext();
|
||||
lexer.Commit();
|
||||
}
|
||||
while (!lexer.EndOfInput && !(lexer.Current == '/' && lexer.Previous == '*'));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
63
src/CSConsole/Lexers/KeywordLexer.cs
Normal file
63
src/CSConsole/Lexers/KeywordLexer.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public class KeywordLexer : Lexer
|
||||
{
|
||||
// system blue
|
||||
protected override Color HighlightColor => new Color(0.33f, 0.61f, 0.83f, 1.0f);
|
||||
|
||||
public static readonly HashSet<string> keywords = new HashSet<string>
|
||||
{
|
||||
// reserved keywords
|
||||
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue",
|
||||
"decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally",
|
||||
"fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock",
|
||||
"long", "namespace", "new", "null", "object", "operator", "out", "override", "params", "private", "protected", "public",
|
||||
"readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", "static", "string", "struct", "switch",
|
||||
"this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", "void",
|
||||
"volatile", "while",
|
||||
// contextual keywords
|
||||
"add", "and", "alias", "ascending", "async", "await", "by", "descending", "dynamic", "equals", "from", "get",
|
||||
"global", "group", "init", "into", "join", "let", "managed", "nameof", "not", "notnull", "on",
|
||||
"or", "orderby", "partial", "record", "remove", "select", "set", "unmanaged", "value", "var", "when", "where",
|
||||
"where", "with", "yield", "nint", "nuint"
|
||||
};
|
||||
|
||||
public override bool TryMatchCurrent(LexerBuilder lexer)
|
||||
{
|
||||
var prev = lexer.Previous;
|
||||
var first = lexer.Current;
|
||||
|
||||
// check for keywords
|
||||
if (lexer.IsDelimiter(prev, true) && char.IsLetter(first))
|
||||
{
|
||||
// can be a keyword...
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(lexer.Current);
|
||||
while (!lexer.EndOfInput && char.IsLetter(lexer.PeekNext()))
|
||||
sb.Append(lexer.Current);
|
||||
|
||||
// next must be whitespace or delimiter
|
||||
if (!lexer.EndOfInput && !(char.IsWhiteSpace(lexer.Current) || lexer.IsDelimiter(lexer.Current)))
|
||||
return false;
|
||||
|
||||
if (keywords.Contains(sb.ToString()))
|
||||
{
|
||||
if (!lexer.EndOfInput)
|
||||
lexer.RollbackBy(1);
|
||||
lexer.Commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
18
src/CSConsole/Lexers/Lexer.cs
Normal file
18
src/CSConsole/Lexers/Lexer.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public abstract class Lexer
|
||||
{
|
||||
public virtual IEnumerable<char> Delimiters => Enumerable.Empty<char>();
|
||||
|
||||
protected abstract Color HighlightColor { get; }
|
||||
|
||||
public string ColorTag => colorTag ?? (colorTag = "<color=#" + HighlightColor.ToHex() + ">");
|
||||
private string colorTag;
|
||||
|
||||
public abstract bool TryMatchCurrent(LexerBuilder lexer);
|
||||
}
|
||||
}
|
32
src/CSConsole/Lexers/NumberLexer.cs
Normal file
32
src/CSConsole/Lexers/NumberLexer.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public class NumberLexer : Lexer
|
||||
{
|
||||
// Maroon
|
||||
protected override Color HighlightColor => new Color(0.58f, 0.33f, 0.33f, 1.0f);
|
||||
|
||||
private bool IsNumeric(char c) => char.IsNumber(c) || c == '.';
|
||||
|
||||
public override bool TryMatchCurrent(LexerBuilder lexer)
|
||||
{
|
||||
// previous character must be whitespace or delimiter
|
||||
if (!lexer.IsDelimiter(lexer.Previous, true))
|
||||
return false;
|
||||
|
||||
if (!IsNumeric(lexer.Current))
|
||||
return false;
|
||||
|
||||
while (!lexer.EndOfInput)
|
||||
{
|
||||
lexer.Commit();
|
||||
if (!IsNumeric(lexer.PeekNext()))
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
80
src/CSConsole/Lexers/StringLexer.cs
Normal file
80
src/CSConsole/Lexers/StringLexer.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public class StringLexer : Lexer
|
||||
{
|
||||
public override IEnumerable<char> Delimiters => new[] { '"', '\'', };
|
||||
|
||||
// orange
|
||||
protected override Color HighlightColor => new Color(0.79f, 0.52f, 0.32f, 1.0f);
|
||||
|
||||
public override bool TryMatchCurrent(LexerBuilder lexer)
|
||||
{
|
||||
if (lexer.Current == '"')
|
||||
{
|
||||
if (lexer.Previous == '@')
|
||||
{
|
||||
// verbatim string, continue until un-escaped quote.
|
||||
while (!lexer.EndOfInput)
|
||||
{
|
||||
lexer.Commit();
|
||||
if (lexer.PeekNext() == '"')
|
||||
{
|
||||
lexer.Commit();
|
||||
// possibly the end, check for escaped quotes.
|
||||
// commit the character and flip the escape bool for each quote.
|
||||
bool escaped = false;
|
||||
while (lexer.PeekNext() == '"')
|
||||
{
|
||||
lexer.Commit();
|
||||
escaped = !escaped;
|
||||
}
|
||||
// if the last quote wasnt escaped, that was the end of the string.
|
||||
if (!escaped)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// normal string
|
||||
// continue until a quote which is not escaped, or end of input
|
||||
|
||||
while (!lexer.EndOfInput)
|
||||
{
|
||||
lexer.Commit();
|
||||
lexer.PeekNext();
|
||||
if ((lexer.Current == '"') && lexer.Previous != '\\')
|
||||
{
|
||||
lexer.Commit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (lexer.Current == '\'')
|
||||
{
|
||||
// char
|
||||
|
||||
while (!lexer.EndOfInput)
|
||||
{
|
||||
lexer.Commit();
|
||||
lexer.PeekNext();
|
||||
if ((lexer.Current == '\'') && lexer.Previous != '\\')
|
||||
{
|
||||
lexer.Commit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
49
src/CSConsole/Lexers/SymbolLexer.cs
Normal file
49
src/CSConsole/Lexers/SymbolLexer.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.CSConsole.Lexers
|
||||
{
|
||||
public class SymbolLexer : Lexer
|
||||
{
|
||||
// silver
|
||||
protected override Color HighlightColor => new Color(0.6f, 0.6f, 0.6f);
|
||||
|
||||
// all symbols are delimiters
|
||||
public override IEnumerable<char> Delimiters => symbols.Where(it => it != '.'); // '.' is not a delimiter, only a separator.
|
||||
|
||||
public static bool IsSymbol(char c) => symbols.Contains(c);
|
||||
|
||||
public static readonly HashSet<char> symbols = new HashSet<char>
|
||||
{
|
||||
'[', '{', '(', // open
|
||||
']', '}', ')', // close
|
||||
'.', ',', ';', ':', '?', '@', // special
|
||||
|
||||
// operators
|
||||
'+', '-', '*', '/', '%', '&', '|', '^', '~', '=', '<', '>', '!',
|
||||
};
|
||||
|
||||
public override bool TryMatchCurrent(LexerBuilder lexer)
|
||||
{
|
||||
// previous character must be delimiter, whitespace, or alphanumeric.
|
||||
if (!lexer.IsDelimiter(lexer.Previous, true, true))
|
||||
return false;
|
||||
|
||||
if (IsSymbol(lexer.Current))
|
||||
{
|
||||
do
|
||||
{
|
||||
lexer.Commit();
|
||||
lexer.PeekNext();
|
||||
}
|
||||
while (IsSymbol(lexer.Current));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using Mono.CSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Mono.CSharp;
|
||||
using System.Text;
|
||||
|
||||
// Thanks to ManlyMarco for most of this
|
||||
// Thanks to ManlyMarco for this
|
||||
|
||||
namespace UnityExplorer.Core.CSharp
|
||||
namespace UnityExplorer.CSConsole
|
||||
{
|
||||
public class ScriptEvaluator : Evaluator, IDisposable
|
||||
{
|
||||
@ -15,14 +16,14 @@ namespace UnityExplorer.Core.CSharp
|
||||
"mscorlib", "System.Core", "System", "System.Xml"
|
||||
};
|
||||
|
||||
internal static TextWriter _textWriter;
|
||||
internal TextWriter _textWriter;
|
||||
internal static StreamReportPrinter _reportPrinter;
|
||||
|
||||
public ScriptEvaluator(TextWriter tw) : base(BuildContext(tw))
|
||||
{
|
||||
_textWriter = tw;
|
||||
|
||||
ImportAppdomainAssemblies(ReferenceAssembly);
|
||||
ImportAppdomainAssemblies(Reference);
|
||||
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
|
||||
}
|
||||
|
||||
@ -39,11 +40,24 @@ namespace UnityExplorer.Core.CSharp
|
||||
if (StdLib.Contains(name))
|
||||
return;
|
||||
|
||||
ReferenceAssembly(args.LoadedAssembly);
|
||||
Reference(args.LoadedAssembly);
|
||||
}
|
||||
|
||||
private void Reference(Assembly asm)
|
||||
{
|
||||
var name = asm.GetName().Name;
|
||||
if (name == "completions")
|
||||
return;
|
||||
ReferenceAssembly(asm);
|
||||
}
|
||||
|
||||
private static CompilerContext context;
|
||||
|
||||
private static CompilerContext BuildContext(TextWriter tw)
|
||||
{
|
||||
if (context != null)
|
||||
return context;
|
||||
|
||||
_reportPrinter = new StreamReportPrinter(tw);
|
||||
|
||||
var settings = new CompilerSettings
|
||||
@ -56,7 +70,7 @@ namespace UnityExplorer.Core.CSharp
|
||||
EnhancedWarnings = false
|
||||
};
|
||||
|
||||
return new CompilerContext(settings, _reportPrinter);
|
||||
return context = new CompilerContext(settings, _reportPrinter);
|
||||
}
|
||||
|
||||
private static void ImportAppdomainAssemblies(Action<Assembly> import)
|
||||
@ -65,9 +79,7 @@ namespace UnityExplorer.Core.CSharp
|
||||
{
|
||||
string name = assembly.GetName().Name;
|
||||
if (StdLib.Contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
import(assembly);
|
||||
}
|
73
src/CSConsole/ScriptInteraction.cs
Normal file
73
src/CSConsole/ScriptInteraction.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using Mono.CSharp;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
|
||||
namespace UnityExplorer.CSConsole
|
||||
{
|
||||
public class ScriptInteraction : InteractiveBase
|
||||
{
|
||||
public static void Log(object message)
|
||||
{
|
||||
ExplorerCore.Log(message);
|
||||
}
|
||||
|
||||
public static object CurrentTarget => InspectorManager.ActiveInspector?.Target;
|
||||
|
||||
public static object[] AllTargets => InspectorManager.Inspectors.Select(it => it.Target).ToArray();
|
||||
|
||||
public static void Inspect(object obj)
|
||||
{
|
||||
InspectorManager.Inspect(obj);
|
||||
}
|
||||
|
||||
public static void Inspect(Type type)
|
||||
{
|
||||
InspectorManager.Inspect(type);
|
||||
}
|
||||
|
||||
public static void Start(IEnumerator ienumerator)
|
||||
{
|
||||
RuntimeProvider.Instance.StartCoroutine(ienumerator);
|
||||
}
|
||||
|
||||
public static void GetUsing()
|
||||
{
|
||||
Log(Evaluator.GetUsing());
|
||||
}
|
||||
|
||||
public static void GetVars()
|
||||
{
|
||||
var vars = Evaluator.GetVars()?.Trim();
|
||||
if (string.IsNullOrEmpty(vars))
|
||||
ExplorerCore.LogWarning("No variables seem to be defined!");
|
||||
else
|
||||
Log(vars);
|
||||
}
|
||||
|
||||
public static void GetClasses()
|
||||
{
|
||||
if (ReflectionUtility.GetFieldInfo(typeof(Evaluator), "source_file")
|
||||
.GetValue(Evaluator) is CompilationSourceFile sourceFile
|
||||
&& sourceFile.Containers.Any())
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"There are {sourceFile.Containers.Count} defined classes:");
|
||||
foreach (TypeDefinition type in sourceFile.Containers.Where(it => it is TypeDefinition))
|
||||
{
|
||||
sb.Append($"\n\n{type.MemberName.Name}:");
|
||||
foreach (var member in type.Members)
|
||||
sb.Append($"\n\t- {member.AttributeTargets}: \"{member.MemberName.Name}\" ({member.ModFlags})");
|
||||
}
|
||||
Log(sb.ToString());
|
||||
}
|
||||
else
|
||||
ExplorerCore.LogWarning("No classes seem to be defined.");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
49
src/CacheObject/CacheConfigEntry.cs
Normal file
49
src/CacheObject/CacheConfigEntry.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityExplorer.Core.Config;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheConfigEntry : CacheObjectBase
|
||||
{
|
||||
public CacheConfigEntry(IConfigElement configElement)
|
||||
{
|
||||
this.RefConfigElement = configElement;
|
||||
this.FallbackType = configElement.ElementType;
|
||||
|
||||
this.NameLabelText = $"<color=cyan>{configElement.Name}</color>" +
|
||||
$"\r\n<color=grey><i>{configElement.Description}</i></color>";
|
||||
this.NameLabelTextRaw = string.Empty;
|
||||
|
||||
configElement.OnValueChangedNotify += UpdateValueFromSource;
|
||||
}
|
||||
|
||||
public IConfigElement RefConfigElement;
|
||||
|
||||
public override bool ShouldAutoEvaluate => true;
|
||||
public override bool HasArguments => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public void UpdateValueFromSource()
|
||||
{
|
||||
//if (RefConfigElement.BoxedValue.Equals(this.Value))
|
||||
// return;
|
||||
|
||||
SetValueFromSource(RefConfigElement.BoxedValue);
|
||||
|
||||
if (this.CellView != null)
|
||||
this.SetDataToCell(CellView);
|
||||
}
|
||||
|
||||
public override void TrySetUserValue(object value)
|
||||
{
|
||||
this.Value = value;
|
||||
RefConfigElement.BoxedValue = value;
|
||||
}
|
||||
|
||||
protected override bool SetCellEvaluateState(CacheObjectCell cell) => false;
|
||||
}
|
||||
}
|
54
src/CacheObject/CacheField.cs
Normal file
54
src/CacheObject/CacheField.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityExplorer.Inspectors;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheField : CacheMember
|
||||
{
|
||||
public FieldInfo FieldInfo { get; internal set; }
|
||||
public override Type DeclaringType => FieldInfo.DeclaringType;
|
||||
public override bool IsStatic => FieldInfo.IsStatic;
|
||||
public override bool CanWrite => m_canWrite ?? (bool)(m_canWrite = !(FieldInfo.IsLiteral && !FieldInfo.IsInitOnly));
|
||||
private bool? m_canWrite;
|
||||
|
||||
public override bool ShouldAutoEvaluate => true;
|
||||
|
||||
public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member)
|
||||
{
|
||||
base.SetInspectorOwner(inspector, member);
|
||||
}
|
||||
|
||||
protected override object TryEvaluate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var ret = FieldInfo.GetValue(DeclaringInstance);
|
||||
HadException = false;
|
||||
LastException = null;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HadException = true;
|
||||
LastException = ex;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TrySetValue(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
FieldInfo.SetValue(DeclaringInstance, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
src/CacheObject/CacheKeyValuePair.cs
Normal file
100
src/CacheObject/CacheKeyValuePair.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheKeyValuePair : CacheObjectBase
|
||||
{
|
||||
//public InteractiveList CurrentList { get; set; }
|
||||
|
||||
public int DictIndex;
|
||||
public object DictKey;
|
||||
public object DisplayedKey;
|
||||
|
||||
public bool KeyInputWanted;
|
||||
public bool InspectWanted;
|
||||
public string KeyLabelText;
|
||||
public string KeyInputText;
|
||||
public string KeyInputTypeText;
|
||||
|
||||
public float DesiredKeyWidth;
|
||||
public float DesiredValueWidth;
|
||||
|
||||
public override bool ShouldAutoEvaluate => true;
|
||||
public override bool HasArguments => false;
|
||||
public override bool CanWrite => Owner.CanWrite;
|
||||
|
||||
public void SetDictOwner(InteractiveDictionary dict, int index)
|
||||
{
|
||||
this.Owner = dict;
|
||||
this.DictIndex = index;
|
||||
}
|
||||
|
||||
public void SetKey(object key)
|
||||
{
|
||||
this.DictKey = key;
|
||||
this.DisplayedKey = key.TryCast();
|
||||
|
||||
var type = DisplayedKey.GetType();
|
||||
if (ParseUtility.CanParse(type))
|
||||
{
|
||||
KeyInputWanted = true;
|
||||
KeyInputText = ParseUtility.ToStringForInput(DisplayedKey, type);
|
||||
KeyInputTypeText = SignatureHighlighter.Parse(type, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyInputWanted = false;
|
||||
InspectWanted = type != typeof(bool) && !type.IsEnum;
|
||||
KeyLabelText = ToStringUtility.ToStringWithType(DisplayedKey, type, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetDataToCell(CacheObjectCell cell)
|
||||
{
|
||||
base.SetDataToCell(cell);
|
||||
|
||||
var kvpCell = cell as CacheKeyValuePairCell;
|
||||
|
||||
kvpCell.NameLabel.text = $"{DictIndex}:";
|
||||
kvpCell.HiddenNameLabel.Text = "";
|
||||
kvpCell.Image.color = DictIndex % 2 == 0 ? CacheListEntryCell.EvenColor : CacheListEntryCell.OddColor;
|
||||
|
||||
if (KeyInputWanted)
|
||||
{
|
||||
kvpCell.KeyInputField.UIRoot.SetActive(true);
|
||||
kvpCell.KeyInputTypeLabel.gameObject.SetActive(true);
|
||||
kvpCell.KeyLabel.gameObject.SetActive(false);
|
||||
kvpCell.KeyInspectButton.Component.gameObject.SetActive(false);
|
||||
|
||||
kvpCell.KeyInputField.Text = KeyInputText;
|
||||
kvpCell.KeyInputTypeLabel.text = KeyInputTypeText;
|
||||
}
|
||||
else
|
||||
{
|
||||
kvpCell.KeyInputField.UIRoot.SetActive(false);
|
||||
kvpCell.KeyInputTypeLabel.gameObject.SetActive(false);
|
||||
kvpCell.KeyLabel.gameObject.SetActive(true);
|
||||
kvpCell.KeyInspectButton.Component.gameObject.SetActive(InspectWanted);
|
||||
|
||||
kvpCell.KeyLabel.text = KeyLabelText;
|
||||
}
|
||||
}
|
||||
|
||||
public override void TrySetUserValue(object value)
|
||||
{
|
||||
(Owner as InteractiveDictionary).TrySetValueToKey(DictKey, value, DictIndex);
|
||||
}
|
||||
|
||||
|
||||
protected override bool SetCellEvaluateState(CacheObjectCell cell)
|
||||
{
|
||||
// not needed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
47
src/CacheObject/CacheListEntry.cs
Normal file
47
src/CacheObject/CacheListEntry.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheListEntry : CacheObjectBase
|
||||
{
|
||||
public int ListIndex;
|
||||
|
||||
public override bool ShouldAutoEvaluate => true;
|
||||
public override bool HasArguments => false;
|
||||
public override bool CanWrite => Owner.CanWrite;
|
||||
|
||||
public void SetListOwner(InteractiveList list, int listIndex)
|
||||
{
|
||||
this.Owner = list;
|
||||
this.ListIndex = listIndex;
|
||||
}
|
||||
|
||||
public override void SetDataToCell(CacheObjectCell cell)
|
||||
{
|
||||
base.SetDataToCell(cell);
|
||||
|
||||
var listCell = cell as CacheListEntryCell;
|
||||
|
||||
listCell.NameLabel.text = $"{ListIndex}:";
|
||||
listCell.HiddenNameLabel.Text = "";
|
||||
listCell.Image.color = ListIndex % 2 == 0 ? CacheListEntryCell.EvenColor : CacheListEntryCell.OddColor;
|
||||
}
|
||||
|
||||
public override void TrySetUserValue(object value)
|
||||
{
|
||||
(Owner as InteractiveList).TrySetValueToIndex(value, this.ListIndex);
|
||||
}
|
||||
|
||||
|
||||
protected override bool SetCellEvaluateState(CacheObjectCell cell)
|
||||
{
|
||||
// not needed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
339
src/CacheObject/CacheMember.cs
Normal file
339
src/CacheObject/CacheMember.cs
Normal file
@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI.Models;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public abstract class CacheMember : CacheObjectBase
|
||||
{
|
||||
public abstract Type DeclaringType { get; }
|
||||
public string NameForFiltering { get; protected set; }
|
||||
public object DeclaringInstance => IsStatic ? null : (m_declaringInstance ?? (m_declaringInstance = Owner.Target.TryCast(DeclaringType)));
|
||||
private object m_declaringInstance;
|
||||
|
||||
public abstract bool IsStatic { get; }
|
||||
public override bool HasArguments => Arguments?.Length > 0 || GenericArguments.Length > 0;
|
||||
public ParameterInfo[] Arguments { get; protected set; } = new ParameterInfo[0];
|
||||
public Type[] GenericArguments { get; protected set; } = ArgumentUtility.EmptyTypes;
|
||||
public EvaluateWidget Evaluator { get; protected set; }
|
||||
public bool Evaluating => Evaluator != null && Evaluator.UIRoot.activeSelf;
|
||||
|
||||
public virtual void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member)
|
||||
{
|
||||
this.Owner = inspector;
|
||||
this.NameLabelText = SignatureHighlighter.Parse(member.DeclaringType, false, member);
|
||||
this.NameForFiltering = $"{member.DeclaringType.Name}.{member.Name}";
|
||||
this.NameLabelTextRaw = NameForFiltering;
|
||||
}
|
||||
|
||||
public override void ReleasePooledObjects()
|
||||
{
|
||||
base.ReleasePooledObjects();
|
||||
|
||||
if (this.Evaluator != null)
|
||||
{
|
||||
this.Evaluator.OnReturnToPool();
|
||||
Pool<EvaluateWidget>.Return(this.Evaluator);
|
||||
this.Evaluator = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void UnlinkFromView()
|
||||
{
|
||||
if (this.Evaluator != null)
|
||||
this.Evaluator.UIRoot.transform.SetParent(Pool<EvaluateWidget>.Instance.InactiveHolder.transform, false);
|
||||
|
||||
base.UnlinkFromView();
|
||||
}
|
||||
|
||||
protected abstract object TryEvaluate();
|
||||
|
||||
protected abstract void TrySetValue(object value);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate is called when first shown (if ShouldAutoEvaluate), or else when Evaluate button is clicked, or auto-updated.
|
||||
/// </summary>
|
||||
public void Evaluate()
|
||||
{
|
||||
SetValueFromSource(TryEvaluate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when user presses the Evaluate button.
|
||||
/// </summary>
|
||||
public void EvaluateAndSetCell()
|
||||
{
|
||||
Evaluate();
|
||||
if (CellView != null)
|
||||
SetDataToCell(CellView);
|
||||
}
|
||||
|
||||
public override void TrySetUserValue(object value)
|
||||
{
|
||||
TrySetValue(value);
|
||||
Evaluate();
|
||||
}
|
||||
|
||||
protected override void SetValueState(CacheObjectCell cell, ValueStateArgs args)
|
||||
{
|
||||
base.SetValueState(cell, args);
|
||||
}
|
||||
|
||||
private static readonly Color evalEnabledColor = new Color(0.15f, 0.25f, 0.15f);
|
||||
private static readonly Color evalDisabledColor = new Color(0.15f, 0.15f, 0.15f);
|
||||
|
||||
protected override bool SetCellEvaluateState(CacheObjectCell objectcell)
|
||||
{
|
||||
var cell = objectcell as CacheMemberCell;
|
||||
|
||||
cell.EvaluateHolder.SetActive(!ShouldAutoEvaluate);
|
||||
if (!ShouldAutoEvaluate)
|
||||
{
|
||||
cell.EvaluateButton.Component.gameObject.SetActive(true);
|
||||
if (HasArguments)
|
||||
{
|
||||
if (!Evaluating)
|
||||
cell.EvaluateButton.ButtonText.text = $"Evaluate ({Arguments.Length + GenericArguments.Length})";
|
||||
else
|
||||
{
|
||||
cell.EvaluateButton.ButtonText.text = "Hide";
|
||||
Evaluator.UIRoot.transform.SetParent(cell.EvaluateHolder.transform, false);
|
||||
RuntimeProvider.Instance.SetColorBlock(cell.EvaluateButton.Component, evalEnabledColor, evalEnabledColor * 1.3f);
|
||||
}
|
||||
}
|
||||
else
|
||||
cell.EvaluateButton.ButtonText.text = "Evaluate";
|
||||
|
||||
if (!Evaluating)
|
||||
RuntimeProvider.Instance.SetColorBlock(cell.EvaluateButton.Component, evalDisabledColor, evalDisabledColor * 1.3f);
|
||||
}
|
||||
|
||||
if (State == ValueState.NotEvaluated && !ShouldAutoEvaluate)
|
||||
{
|
||||
SetValueState(cell, ValueStateArgs.Default);
|
||||
cell.RefreshSubcontentButton();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (State == ValueState.NotEvaluated)
|
||||
Evaluate();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnEvaluateClicked()
|
||||
{
|
||||
if (!HasArguments)
|
||||
{
|
||||
EvaluateAndSetCell();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Evaluator == null)
|
||||
{
|
||||
this.Evaluator = Pool<EvaluateWidget>.Borrow();
|
||||
Evaluator.OnBorrowedFromPool(this);
|
||||
Evaluator.UIRoot.transform.SetParent((CellView as CacheMemberCell).EvaluateHolder.transform, false);
|
||||
SetCellEvaluateState(CellView);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Evaluator.UIRoot.activeSelf)
|
||||
Evaluator.UIRoot.SetActive(false);
|
||||
else
|
||||
Evaluator.UIRoot.SetActive(true);
|
||||
|
||||
SetCellEvaluateState(CellView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region Cache Member Util
|
||||
|
||||
public static bool CanParseArgs(ParameterInfo[] parameters)
|
||||
{
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
var pType = param.ParameterType;
|
||||
|
||||
if (pType.IsByRef && pType.HasElementType)
|
||||
pType = pType.GetElementType();
|
||||
|
||||
if (pType != null && ParseUtility.CanParse(pType))
|
||||
continue;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<CacheMember> GetCacheMembers(object inspectorTarget, Type _type, ReflectionInspector _inspector)
|
||||
{
|
||||
var list = new List<CacheMember>();
|
||||
var cachedSigs = new HashSet<string>();
|
||||
|
||||
var types = ReflectionUtility.GetAllBaseTypes(_type);
|
||||
|
||||
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
|
||||
if (!_inspector.StaticOnly)
|
||||
flags |= BindingFlags.Instance;
|
||||
|
||||
var infos = new List<MemberInfo>();
|
||||
|
||||
foreach (var declaringType in types)
|
||||
{
|
||||
var target = inspectorTarget;
|
||||
if (!_inspector.StaticOnly)
|
||||
target = target.TryCast(declaringType);
|
||||
|
||||
infos.Clear();
|
||||
infos.AddRange(declaringType.GetProperties(flags));
|
||||
infos.AddRange(declaringType.GetFields(flags));
|
||||
infos.AddRange(declaringType.GetMethods(flags));
|
||||
|
||||
foreach (var member in infos)
|
||||
{
|
||||
if (member.DeclaringType != declaringType)
|
||||
continue;
|
||||
TryCacheMember(member, list, cachedSigs, declaringType, _inspector);
|
||||
}
|
||||
}
|
||||
|
||||
var typeList = types.ToList();
|
||||
|
||||
var sorted = new List<CacheMember>();
|
||||
sorted.AddRange(list.Where(it => it is CacheProperty)
|
||||
.OrderBy(it => typeList.IndexOf(it.DeclaringType))
|
||||
.ThenBy(it => it.NameForFiltering));
|
||||
sorted.AddRange(list.Where(it => it is CacheField)
|
||||
.OrderBy(it => typeList.IndexOf(it.DeclaringType))
|
||||
.ThenBy(it => it.NameForFiltering));
|
||||
sorted.AddRange(list.Where(it => it is CacheMethod)
|
||||
.OrderBy(it => typeList.IndexOf(it.DeclaringType))
|
||||
.ThenBy(it => it.NameForFiltering));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static void TryCacheMember(MemberInfo member, List<CacheMember> list, HashSet<string> cachedSigs,
|
||||
Type declaringType, ReflectionInspector _inspector, bool ignorePropertyMethodInfos = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ReflectionUtility.IsBlacklisted(member))
|
||||
return;
|
||||
|
||||
var sig = GetSig(member);
|
||||
|
||||
//ExplorerCore.Log($"Trying to cache member {sig}... ({member.MemberType})");
|
||||
|
||||
CacheMember cached;
|
||||
Type returnType;
|
||||
switch (member.MemberType)
|
||||
{
|
||||
case MemberTypes.Method:
|
||||
{
|
||||
var mi = member as MethodInfo;
|
||||
if (ignorePropertyMethodInfos
|
||||
&& (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_")))
|
||||
return;
|
||||
|
||||
var args = mi.GetParameters();
|
||||
if (!CanParseArgs(args))
|
||||
return;
|
||||
|
||||
sig += GetArgumentString(args);
|
||||
if (cachedSigs.Contains(sig))
|
||||
return;
|
||||
|
||||
cached = new CacheMethod() { MethodInfo = mi };
|
||||
returnType = mi.ReturnType;
|
||||
break;
|
||||
}
|
||||
|
||||
case MemberTypes.Property:
|
||||
{
|
||||
var pi = member as PropertyInfo;
|
||||
|
||||
var args = pi.GetIndexParameters();
|
||||
if (!CanParseArgs(args))
|
||||
return;
|
||||
|
||||
if (!pi.CanRead && pi.CanWrite)
|
||||
{
|
||||
// write-only property, cache the set method instead.
|
||||
var setMethod = pi.GetSetMethod(true);
|
||||
if (setMethod != null)
|
||||
TryCacheMember(setMethod, list, cachedSigs, declaringType, _inspector, false);
|
||||
return;
|
||||
}
|
||||
|
||||
sig += GetArgumentString(args);
|
||||
if (cachedSigs.Contains(sig))
|
||||
return;
|
||||
|
||||
cached = new CacheProperty() { PropertyInfo = pi };
|
||||
returnType = pi.PropertyType;
|
||||
break;
|
||||
}
|
||||
|
||||
case MemberTypes.Field:
|
||||
{
|
||||
var fi = member as FieldInfo;
|
||||
cached = new CacheField() { FieldInfo = fi };
|
||||
returnType = fi.FieldType;
|
||||
break;
|
||||
}
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
cachedSigs.Add(sig);
|
||||
|
||||
cached.SetFallbackType(returnType);
|
||||
cached.SetInspectorOwner(_inspector, member);
|
||||
|
||||
list.Add(cached);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!");
|
||||
ExplorerCore.Log(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetSig(MemberInfo member)
|
||||
=> $"{member.DeclaringType.Name}.{member.Name}";
|
||||
|
||||
internal static string GetArgumentString(ParameterInfo[] args)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(' ');
|
||||
sb.Append('(');
|
||||
foreach (var param in args)
|
||||
{
|
||||
sb.Append(param.ParameterType.Name);
|
||||
sb.Append(' ');
|
||||
sb.Append(param.Name);
|
||||
sb.Append(',');
|
||||
sb.Append(' ');
|
||||
}
|
||||
sb.Append(')');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
55
src/CacheObject/CacheMethod.cs
Normal file
55
src/CacheObject/CacheMethod.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityExplorer.Inspectors;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheMethod : CacheMember
|
||||
{
|
||||
public MethodInfo MethodInfo { get; internal set; }
|
||||
public override Type DeclaringType => MethodInfo.DeclaringType;
|
||||
public override bool CanWrite => false;
|
||||
public override bool IsStatic => MethodInfo.IsStatic;
|
||||
|
||||
public override bool ShouldAutoEvaluate => false;
|
||||
|
||||
public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member)
|
||||
{
|
||||
base.SetInspectorOwner(inspector, member);
|
||||
|
||||
Arguments = MethodInfo.GetParameters();
|
||||
if (MethodInfo.IsGenericMethod)
|
||||
GenericArguments = MethodInfo.GetGenericArguments();
|
||||
}
|
||||
|
||||
protected override object TryEvaluate()
|
||||
{
|
||||
try
|
||||
{
|
||||
var methodInfo = MethodInfo;
|
||||
if (methodInfo.IsGenericMethod)
|
||||
methodInfo = MethodInfo.MakeGenericMethod(Evaluator.TryParseGenericArguments());
|
||||
|
||||
object ret;
|
||||
if (HasArguments)
|
||||
ret = methodInfo.Invoke(DeclaringInstance, Evaluator.TryParseArguments());
|
||||
else
|
||||
ret = methodInfo.Invoke(DeclaringInstance, ArgumentUtility.EmptyArgs);
|
||||
HadException = false;
|
||||
LastException = null;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HadException = true;
|
||||
LastException = ex;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TrySetValue(object value) => throw new NotImplementedException("You can't set a method");
|
||||
}
|
||||
}
|
481
src/CacheObject/CacheObjectBase.cs
Normal file
481
src/CacheObject/CacheObjectBase.cs
Normal file
@ -0,0 +1,481 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
using UnityExplorer.UI.Models;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public enum ValueState
|
||||
{
|
||||
NotEvaluated,
|
||||
Exception,
|
||||
Boolean,
|
||||
Number,
|
||||
String,
|
||||
Enum,
|
||||
Collection,
|
||||
Dictionary,
|
||||
ValueStruct,
|
||||
Color,
|
||||
Unsupported
|
||||
}
|
||||
|
||||
public abstract class CacheObjectBase
|
||||
{
|
||||
public ICacheObjectController Owner { get; set; }
|
||||
|
||||
public CacheObjectCell CellView { get; internal set; }
|
||||
|
||||
public object Value { get; protected set; }
|
||||
public Type FallbackType { get; protected set; }
|
||||
public bool LastValueWasNull { get; private set; }
|
||||
|
||||
public ValueState State = ValueState.NotEvaluated;
|
||||
public Type LastValueType;
|
||||
|
||||
public InteractiveValue IValue { get; private set; }
|
||||
public Type CurrentIValueType { get; private set; }
|
||||
public bool SubContentShowWanted { get; private set; }
|
||||
|
||||
public string NameLabelText { get; protected set; }
|
||||
public string NameLabelTextRaw { get; protected set; }
|
||||
public string ValueLabelText { get; protected set; }
|
||||
|
||||
public abstract bool ShouldAutoEvaluate { get; }
|
||||
public abstract bool HasArguments { get; }
|
||||
public abstract bool CanWrite { get; }
|
||||
public bool HadException { get; protected set; }
|
||||
public Exception LastException { get; protected set; }
|
||||
|
||||
public virtual void SetFallbackType(Type fallbackType)
|
||||
{
|
||||
this.FallbackType = fallbackType;
|
||||
this.ValueLabelText = GetValueLabel();
|
||||
}
|
||||
|
||||
protected const string NOT_YET_EVAL = "<color=grey>Not yet evaluated</color>";
|
||||
|
||||
public virtual void ReleasePooledObjects()
|
||||
{
|
||||
if (this.IValue != null)
|
||||
ReleaseIValue();
|
||||
|
||||
if (this.CellView != null)
|
||||
UnlinkFromView();
|
||||
}
|
||||
|
||||
public virtual void SetView(CacheObjectCell cellView)
|
||||
{
|
||||
this.CellView = cellView;
|
||||
cellView.Occupant = this;
|
||||
}
|
||||
|
||||
public virtual void UnlinkFromView()
|
||||
{
|
||||
if (this.CellView == null)
|
||||
return;
|
||||
|
||||
this.CellView.Occupant = null;
|
||||
this.CellView = null;
|
||||
|
||||
if (this.IValue != null)
|
||||
this.IValue.UIRoot.transform.SetParent(InactiveIValueHolder.transform, false);
|
||||
}
|
||||
|
||||
// Updating and applying values
|
||||
|
||||
public void SetUserValue(object value)
|
||||
{
|
||||
value = value.TryCast(FallbackType);
|
||||
|
||||
TrySetUserValue(value);
|
||||
|
||||
if (CellView != null)
|
||||
SetDataToCell(CellView);
|
||||
|
||||
// If the owner's parent CacheObject is set, we are setting the value of an inspected struct.
|
||||
// Set the inspector target as the value back to that parent cacheobject.
|
||||
if (Owner.ParentCacheObject != null)
|
||||
Owner.ParentCacheObject.SetUserValue(Owner.Target);
|
||||
}
|
||||
|
||||
public abstract void TrySetUserValue(object value);
|
||||
|
||||
// The only method which sets the CacheObjectBase.Value
|
||||
public virtual void SetValueFromSource(object value)
|
||||
{
|
||||
this.Value = value;
|
||||
|
||||
if (!Value.IsNullOrDestroyed())
|
||||
Value = Value.TryCast();
|
||||
|
||||
ProcessOnEvaluate();
|
||||
|
||||
if (this.IValue != null)
|
||||
{
|
||||
if (SubContentShowWanted)
|
||||
this.IValue.SetValue(Value);
|
||||
else
|
||||
IValue.PendingValueWanted = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ProcessOnEvaluate()
|
||||
{
|
||||
var prevState = State;
|
||||
|
||||
if (HadException)
|
||||
{
|
||||
LastValueWasNull = true;
|
||||
LastValueType = FallbackType;
|
||||
State = ValueState.Exception;
|
||||
}
|
||||
else if (Value.IsNullOrDestroyed())
|
||||
{
|
||||
LastValueWasNull = true;
|
||||
State = GetStateForType(FallbackType);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastValueWasNull = false;
|
||||
State = GetStateForType(Value.GetActualType());
|
||||
}
|
||||
|
||||
if (IValue != null)
|
||||
{
|
||||
// If we changed states (always needs IValue change)
|
||||
// or if the value is null, and the fallback type isnt string (we always want to edit strings).
|
||||
if (State != prevState || (State != ValueState.String && Value.IsNullOrDestroyed()))
|
||||
{
|
||||
// need to return IValue
|
||||
ReleaseIValue();
|
||||
SubContentShowWanted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set label text
|
||||
this.ValueLabelText = GetValueLabel();
|
||||
}
|
||||
|
||||
public ValueState GetStateForType(Type type)
|
||||
{
|
||||
if (LastValueType == type)
|
||||
return State;
|
||||
|
||||
LastValueType = type;
|
||||
if (type == typeof(bool))
|
||||
return ValueState.Boolean;
|
||||
else if (type.IsPrimitive || type == typeof(decimal))
|
||||
return ValueState.Number;
|
||||
else if (type == typeof(string))
|
||||
return ValueState.String;
|
||||
else if (type.IsEnum)
|
||||
return ValueState.Enum;
|
||||
else if (type == typeof(Color) || type == typeof(Color32))
|
||||
return ValueState.Color;
|
||||
else if (InteractiveValueStruct.SupportsType(type))
|
||||
return ValueState.ValueStruct;
|
||||
else if (ReflectionUtility.IsDictionary(type))
|
||||
return ValueState.Dictionary;
|
||||
else if (!typeof(Transform).IsAssignableFrom(type) && ReflectionUtility.IsEnumerable(type))
|
||||
return ValueState.Collection;
|
||||
else
|
||||
return ValueState.Unsupported;
|
||||
}
|
||||
|
||||
protected string GetValueLabel()
|
||||
{
|
||||
string label = "";
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ValueState.NotEvaluated:
|
||||
return $"<i>{NOT_YET_EVAL} ({SignatureHighlighter.Parse(FallbackType, true)})</i>";
|
||||
|
||||
case ValueState.Exception:
|
||||
return $"<i><color=red>{LastException.ReflectionExToString()}</color></i>";
|
||||
|
||||
// bool and number dont want the label for the value at all
|
||||
case ValueState.Boolean:
|
||||
case ValueState.Number:
|
||||
return null;
|
||||
|
||||
// and valuestruct also doesnt want it if we can parse it
|
||||
case ValueState.ValueStruct:
|
||||
if (ParseUtility.CanParse(LastValueType))
|
||||
return null;
|
||||
break;
|
||||
|
||||
// string wants it trimmed to max 200 chars
|
||||
case ValueState.String:
|
||||
if (!LastValueWasNull)
|
||||
{
|
||||
string s = Value as string;
|
||||
return $"\"{ToStringUtility.PruneString(s, 200, 5)}\"";
|
||||
}
|
||||
break;
|
||||
|
||||
// try to prefix the count of the collection for lists and dicts
|
||||
case ValueState.Collection:
|
||||
if (!LastValueWasNull)
|
||||
{
|
||||
if (Value is IList iList)
|
||||
label = $"[{iList.Count}] ";
|
||||
else if (Value is ICollection iCol)
|
||||
label = $"[{iCol.Count}] ";
|
||||
else
|
||||
label = "[?] ";
|
||||
}
|
||||
break;
|
||||
|
||||
case ValueState.Dictionary:
|
||||
if (!LastValueWasNull)
|
||||
{
|
||||
if (Value is IDictionary iDict)
|
||||
label = $"[{iDict.Count}] ";
|
||||
else
|
||||
label = "[?] ";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Cases which dont return will append to ToStringWithType
|
||||
|
||||
return label += ToStringUtility.ToStringWithType(Value, FallbackType, true);
|
||||
}
|
||||
|
||||
// Setting cell state from our model
|
||||
|
||||
/// <summary>Return true if SetCell should abort, false if it should continue.</summary>
|
||||
protected abstract bool SetCellEvaluateState(CacheObjectCell cell);
|
||||
|
||||
public virtual void SetDataToCell(CacheObjectCell cell)
|
||||
{
|
||||
cell.NameLabel.text = NameLabelText;
|
||||
if (cell.HiddenNameLabel != null)
|
||||
cell.HiddenNameLabel.Text = NameLabelTextRaw;
|
||||
cell.ValueLabel.gameObject.SetActive(true);
|
||||
|
||||
cell.SubContentHolder.gameObject.SetActive(SubContentShowWanted);
|
||||
if (IValue != null)
|
||||
{
|
||||
IValue.UIRoot.transform.SetParent(cell.SubContentHolder.transform, false);
|
||||
IValue.SetLayout();
|
||||
}
|
||||
|
||||
if (SetCellEvaluateState(cell))
|
||||
return;
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ValueState.Exception:
|
||||
SetValueState(cell, ValueStateArgs.Default);
|
||||
break;
|
||||
case ValueState.Boolean:
|
||||
SetValueState(cell, new ValueStateArgs(false, toggleActive: true, applyActive: CanWrite));
|
||||
break;
|
||||
case ValueState.Number:
|
||||
SetValueState(cell, new ValueStateArgs(false, typeLabelActive: true, inputActive: true, applyActive: CanWrite));
|
||||
break;
|
||||
case ValueState.String:
|
||||
if (LastValueWasNull)
|
||||
SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: true));
|
||||
else
|
||||
SetValueState(cell, new ValueStateArgs(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true));
|
||||
break;
|
||||
case ValueState.Enum:
|
||||
SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: CanWrite));
|
||||
break;
|
||||
case ValueState.Color:
|
||||
case ValueState.ValueStruct:
|
||||
if (ParseUtility.CanParse(LastValueType))
|
||||
SetValueState(cell, new ValueStateArgs(false, false, null, true, false, true, CanWrite, true, true));
|
||||
else
|
||||
SetValueState(cell, new ValueStateArgs(true, inspectActive: true, subContentButtonActive: true));
|
||||
break;
|
||||
case ValueState.Collection:
|
||||
case ValueState.Dictionary:
|
||||
SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull, subContentButtonActive: !LastValueWasNull));
|
||||
break;
|
||||
case ValueState.Unsupported:
|
||||
SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull));
|
||||
break;
|
||||
}
|
||||
|
||||
cell.RefreshSubcontentButton();
|
||||
}
|
||||
|
||||
protected virtual void SetValueState(CacheObjectCell cell, ValueStateArgs args)
|
||||
{
|
||||
// main value label
|
||||
if (args.valueActive)
|
||||
{
|
||||
cell.ValueLabel.text = ValueLabelText;
|
||||
cell.ValueLabel.supportRichText = args.valueRichText;
|
||||
cell.ValueLabel.color = args.valueColor;
|
||||
}
|
||||
else
|
||||
cell.ValueLabel.text = "";
|
||||
|
||||
// Type label (for primitives)
|
||||
cell.TypeLabel.gameObject.SetActive(args.typeLabelActive);
|
||||
if (args.typeLabelActive)
|
||||
cell.TypeLabel.text = SignatureHighlighter.Parse(LastValueType, false);
|
||||
|
||||
// toggle for bools
|
||||
cell.Toggle.gameObject.SetActive(args.toggleActive);
|
||||
if (args.toggleActive)
|
||||
{
|
||||
cell.Toggle.interactable = CanWrite;
|
||||
cell.Toggle.isOn = (bool)Value;
|
||||
cell.ToggleText.text = Value.ToString();
|
||||
}
|
||||
|
||||
// inputfield for numbers
|
||||
cell.InputField.UIRoot.SetActive(args.inputActive);
|
||||
if (args.inputActive)
|
||||
{
|
||||
cell.InputField.Text = ParseUtility.ToStringForInput(Value, LastValueType);
|
||||
cell.InputField.Component.readOnly = !CanWrite;
|
||||
}
|
||||
|
||||
// apply for bool and numbers
|
||||
cell.ApplyButton.Component.gameObject.SetActive(args.applyActive);
|
||||
|
||||
// Inspect button only if last value not null.
|
||||
if (cell.InspectButton != null)
|
||||
cell.InspectButton.Component.gameObject.SetActive(args.inspectActive && !LastValueWasNull);
|
||||
|
||||
// allow IValue for null strings though
|
||||
cell.SubContentButton.Component.gameObject.SetActive(args.subContentButtonActive && (!LastValueWasNull || State == ValueState.String));
|
||||
}
|
||||
|
||||
// CacheObjectCell Apply
|
||||
|
||||
public virtual void OnCellApplyClicked()
|
||||
{
|
||||
if (State == ValueState.Boolean)
|
||||
SetUserValue(this.CellView.Toggle.isOn);
|
||||
else
|
||||
{
|
||||
if (ParseUtility.TryParse(CellView.InputField.Text, LastValueType, out object value, out Exception ex))
|
||||
{
|
||||
SetUserValue(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ExplorerCore.LogWarning("Unable to parse input!");
|
||||
if (ex != null)
|
||||
ExplorerCore.Log(ex.ReflectionExToString());
|
||||
}
|
||||
}
|
||||
|
||||
SetDataToCell(this.CellView);
|
||||
}
|
||||
|
||||
// IValues
|
||||
|
||||
public virtual void OnCellSubContentToggle()
|
||||
{
|
||||
if (this.IValue == null)
|
||||
{
|
||||
var ivalueType = InteractiveValue.GetIValueTypeForState(State);
|
||||
|
||||
if (ivalueType == null)
|
||||
return;
|
||||
|
||||
IValue = (InteractiveValue)Pool.Borrow(ivalueType);
|
||||
CurrentIValueType = ivalueType;
|
||||
|
||||
IValue.OnBorrowed(this);
|
||||
IValue.SetValue(this.Value);
|
||||
IValue.UIRoot.transform.SetParent(CellView.SubContentHolder.transform, false);
|
||||
CellView.SubContentHolder.SetActive(true);
|
||||
SubContentShowWanted = true;
|
||||
|
||||
// update our cell after creating the ivalue (the value may have updated, make sure its consistent)
|
||||
this.ProcessOnEvaluate();
|
||||
this.SetDataToCell(this.CellView);
|
||||
}
|
||||
else
|
||||
{
|
||||
SubContentShowWanted = !SubContentShowWanted;
|
||||
CellView.SubContentHolder.SetActive(SubContentShowWanted);
|
||||
|
||||
if (SubContentShowWanted && IValue.PendingValueWanted)
|
||||
{
|
||||
IValue.PendingValueWanted = false;
|
||||
this.ProcessOnEvaluate();
|
||||
this.SetDataToCell(this.CellView);
|
||||
IValue.SetValue(this.Value);
|
||||
}
|
||||
}
|
||||
|
||||
CellView.RefreshSubcontentButton();
|
||||
}
|
||||
|
||||
public virtual void ReleaseIValue()
|
||||
{
|
||||
if (IValue == null)
|
||||
return;
|
||||
|
||||
IValue.ReleaseFromOwner();
|
||||
Pool.Return(CurrentIValueType, IValue);
|
||||
|
||||
IValue = null;
|
||||
}
|
||||
|
||||
internal static GameObject InactiveIValueHolder
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!inactiveIValueHolder)
|
||||
{
|
||||
inactiveIValueHolder = new GameObject("Temp_IValue_Holder");
|
||||
GameObject.DontDestroyOnLoad(inactiveIValueHolder);
|
||||
inactiveIValueHolder.transform.parent = UIManager.PoolHolder.transform;
|
||||
inactiveIValueHolder.SetActive(false);
|
||||
}
|
||||
return inactiveIValueHolder;
|
||||
}
|
||||
}
|
||||
private static GameObject inactiveIValueHolder;
|
||||
|
||||
// Value state args helper
|
||||
|
||||
public struct ValueStateArgs
|
||||
{
|
||||
public ValueStateArgs(bool valueActive = true, bool valueRichText = true, Color? valueColor = null,
|
||||
bool typeLabelActive = false, bool toggleActive = false, bool inputActive = false, bool applyActive = false,
|
||||
bool inspectActive = false, bool subContentButtonActive = false)
|
||||
{
|
||||
this.valueActive = valueActive;
|
||||
this.valueRichText = valueRichText;
|
||||
this.valueColor = valueColor == null ? Color.white : (Color)valueColor;
|
||||
this.typeLabelActive = typeLabelActive;
|
||||
this.toggleActive = toggleActive;
|
||||
this.inputActive = inputActive;
|
||||
this.applyActive = applyActive;
|
||||
this.inspectActive = inspectActive;
|
||||
this.subContentButtonActive = subContentButtonActive;
|
||||
}
|
||||
|
||||
public static ValueStateArgs Default => _default;
|
||||
private static ValueStateArgs _default = new ValueStateArgs(true);
|
||||
|
||||
public bool valueActive, valueRichText, typeLabelActive, toggleActive,
|
||||
inputActive, applyActive, inspectActive, subContentButtonActive;
|
||||
|
||||
public Color valueColor;
|
||||
}
|
||||
}
|
||||
}
|
68
src/CacheObject/CacheProperty.cs
Normal file
68
src/CacheObject/CacheProperty.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityExplorer.Inspectors;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public class CacheProperty : CacheMember
|
||||
{
|
||||
public PropertyInfo PropertyInfo { get; internal set; }
|
||||
public override Type DeclaringType => PropertyInfo.DeclaringType;
|
||||
public override bool CanWrite => PropertyInfo.CanWrite;
|
||||
public override bool IsStatic => m_isStatic ?? (bool)(m_isStatic = PropertyInfo.GetAccessors(true)[0].IsStatic);
|
||||
private bool? m_isStatic;
|
||||
|
||||
public override bool ShouldAutoEvaluate => !HasArguments;
|
||||
|
||||
public override void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member)
|
||||
{
|
||||
base.SetInspectorOwner(inspector, member);
|
||||
|
||||
Arguments = PropertyInfo.GetIndexParameters();
|
||||
}
|
||||
|
||||
protected override object TryEvaluate()
|
||||
{
|
||||
try
|
||||
{
|
||||
object ret;
|
||||
if (HasArguments)
|
||||
ret = PropertyInfo.GetValue(DeclaringInstance, this.Evaluator.TryParseArguments());
|
||||
else
|
||||
ret = PropertyInfo.GetValue(DeclaringInstance, null);
|
||||
HadException = false;
|
||||
LastException = null;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HadException = true;
|
||||
LastException = ex;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TrySetValue(object value)
|
||||
{
|
||||
if (!CanWrite)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
bool _static = PropertyInfo.GetAccessors(true)[0].IsStatic;
|
||||
|
||||
if (HasArguments)
|
||||
PropertyInfo.SetValue(DeclaringInstance, value, Evaluator.TryParseArguments());
|
||||
else
|
||||
PropertyInfo.SetValue(DeclaringInstance, value, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
src/CacheObject/ICacheObjectController.cs
Normal file
52
src/CacheObject/ICacheObjectController.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
|
||||
namespace UnityExplorer.CacheObject
|
||||
{
|
||||
public interface ICacheObjectController
|
||||
{
|
||||
CacheObjectBase ParentCacheObject { get; }
|
||||
|
||||
object Target { get; }
|
||||
Type TargetType { get; }
|
||||
|
||||
bool CanWrite { get; }
|
||||
}
|
||||
|
||||
public static class CacheObjectControllerHelper
|
||||
{
|
||||
// Helper so that this doesn't need to be copy+pasted between each implementation of the interface
|
||||
|
||||
public static void SetCell(CacheObjectCell cell, int index, IList cachedEntries, Action<CacheObjectCell> onDataSetToCell)
|
||||
{
|
||||
if (index < 0 || index >= cachedEntries.Count)
|
||||
{
|
||||
if (cell.Occupant != null)
|
||||
cell.Occupant.UnlinkFromView();
|
||||
|
||||
cell.Disable();
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = (CacheObjectBase)cachedEntries[index];
|
||||
|
||||
if (entry.CellView != null && entry.CellView != cell)
|
||||
entry.UnlinkFromView();
|
||||
|
||||
if (cell.Occupant != null && cell.Occupant != entry)
|
||||
cell.Occupant.UnlinkFromView();
|
||||
|
||||
if (entry.CellView != cell)
|
||||
entry.SetView(cell);
|
||||
|
||||
entry.SetDataToCell(cell);
|
||||
|
||||
onDataSetToCell?.Invoke(cell);
|
||||
}
|
||||
}
|
||||
}
|
203
src/CacheObject/IValues/InteractiveColor.cs
Normal file
203
src/CacheObject/IValues/InteractiveColor.cs
Normal file
@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveColor : InteractiveValue
|
||||
{
|
||||
public bool IsValueColor32;
|
||||
|
||||
public Color EditedColor;
|
||||
|
||||
private Image m_colorImage;
|
||||
private readonly InputFieldRef[] m_inputs = new InputFieldRef[4];
|
||||
private readonly Slider[] m_sliders = new Slider[4];
|
||||
|
||||
private ButtonRef m_applyButton;
|
||||
|
||||
private static readonly string[] fieldNames = new[] { "R", "G", "B", "A" };
|
||||
|
||||
public override void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
base.OnBorrowed(owner);
|
||||
|
||||
m_applyButton.Component.gameObject.SetActive(owner.CanWrite);
|
||||
|
||||
foreach (var slider in m_sliders)
|
||||
slider.interactable = owner.CanWrite;
|
||||
foreach (var input in m_inputs)
|
||||
input.Component.readOnly = !owner.CanWrite;
|
||||
}
|
||||
|
||||
// owner setting value to this
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
OnOwnerSetValue(value);
|
||||
}
|
||||
|
||||
private void OnOwnerSetValue(object value)
|
||||
{
|
||||
if (value is Color32 c32)
|
||||
{
|
||||
IsValueColor32 = true;
|
||||
EditedColor = c32;
|
||||
m_inputs[0].Text = c32.r.ToString();
|
||||
m_inputs[1].Text = c32.g.ToString();
|
||||
m_inputs[2].Text = c32.b.ToString();
|
||||
m_inputs[3].Text = c32.a.ToString();
|
||||
foreach (var slider in m_sliders)
|
||||
slider.maxValue = 255;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsValueColor32 = false;
|
||||
EditedColor = (Color)value;
|
||||
m_inputs[0].Text = EditedColor.r.ToString();
|
||||
m_inputs[1].Text = EditedColor.g.ToString();
|
||||
m_inputs[2].Text = EditedColor.b.ToString();
|
||||
m_inputs[3].Text = EditedColor.a.ToString();
|
||||
foreach (var slider in m_sliders)
|
||||
slider.maxValue = 1;
|
||||
}
|
||||
|
||||
if (m_colorImage)
|
||||
m_colorImage.color = EditedColor;
|
||||
}
|
||||
|
||||
// setting value to owner
|
||||
|
||||
public void SetValueToOwner()
|
||||
{
|
||||
if (IsValueColor32)
|
||||
CurrentOwner.SetUserValue((Color32)EditedColor);
|
||||
else
|
||||
CurrentOwner.SetUserValue(EditedColor);
|
||||
}
|
||||
|
||||
private void SetColorField(float val, int fieldIndex)
|
||||
{
|
||||
switch (fieldIndex)
|
||||
{
|
||||
case 0: EditedColor.r = val; break;
|
||||
case 1: EditedColor.g = val; break;
|
||||
case 2: EditedColor.b = val; break;
|
||||
case 3: EditedColor.a = val; break;
|
||||
}
|
||||
|
||||
if (m_colorImage)
|
||||
m_colorImage.color = EditedColor;
|
||||
}
|
||||
|
||||
private void OnInputChanged(string val, int fieldIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
float f;
|
||||
if (IsValueColor32)
|
||||
{
|
||||
byte value = byte.Parse(val);
|
||||
m_sliders[fieldIndex].value = value;
|
||||
f = (float)((decimal)value / 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
f = float.Parse(val);
|
||||
m_sliders[fieldIndex].value = f;
|
||||
}
|
||||
|
||||
SetColorField(f, fieldIndex);
|
||||
}
|
||||
catch (ArgumentException) { } // ignore bad user input
|
||||
catch (FormatException) { }
|
||||
catch (OverflowException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("InteractiveColor OnInput: " + ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSliderValueChanged(float val, int fieldIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsValueColor32)
|
||||
{
|
||||
m_inputs[fieldIndex].Text = ((byte)val).ToString();
|
||||
val /= 255f;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_inputs[fieldIndex].Text = val.ToString();
|
||||
}
|
||||
|
||||
SetColorField(val, fieldIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("InteractiveColor OnSlider: " + ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// UI Construction
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveColor", false, false, true, true, 3, new Vector4(4, 4, 4, 4),
|
||||
new Color(0.06f, 0.06f, 0.06f));
|
||||
|
||||
// hori group
|
||||
|
||||
var horiGroup = UIFactory.CreateHorizontalGroup(UIRoot, "ColorEditor", false, false, true, true, 5,
|
||||
default, new Color(1, 1, 1, 0), TextAnchor.MiddleLeft);
|
||||
|
||||
// sliders / inputs
|
||||
|
||||
var grid = UIFactory.CreateGridGroup(horiGroup, "Grid", new Vector2(140, 25), new Vector2(2, 2), new Color(1, 1, 1, 0));
|
||||
UIFactory.SetLayoutElement(grid, minWidth: 580, minHeight: 25, flexibleWidth: 0);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
AddEditorRow(i, grid);
|
||||
|
||||
// apply button
|
||||
|
||||
m_applyButton = UIFactory.CreateButton(horiGroup, "ApplyButton", "Apply", new Color(0.2f, 0.26f, 0.2f));
|
||||
UIFactory.SetLayoutElement(m_applyButton.Component.gameObject, minHeight: 25, minWidth: 90);
|
||||
m_applyButton.OnClick += SetValueToOwner;
|
||||
|
||||
// image of color
|
||||
|
||||
var imgObj = UIFactory.CreateUIObject("ColorImageHelper", horiGroup);
|
||||
UIFactory.SetLayoutElement(imgObj, minHeight: 25, minWidth: 50, flexibleWidth: 50);
|
||||
m_colorImage = imgObj.AddComponent<Image>();
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
|
||||
internal void AddEditorRow(int index, GameObject groupObj)
|
||||
{
|
||||
var row = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow_" + fieldNames[index],
|
||||
false, true, true, true, 5, default, new Color(1, 1, 1, 0));
|
||||
|
||||
var label = UIFactory.CreateLabel(row, "RowLabel", $"{fieldNames[index]}:", TextAnchor.MiddleRight, Color.cyan);
|
||||
UIFactory.SetLayoutElement(label.gameObject, minWidth: 17, flexibleWidth: 0, minHeight: 25);
|
||||
|
||||
var input = UIFactory.CreateInputField(row, "Input", "...");
|
||||
UIFactory.SetLayoutElement(input.UIRoot, minWidth: 40, minHeight: 25, flexibleHeight: 0);
|
||||
m_inputs[index] = input;
|
||||
input.OnValueChanged += (string val) => { OnInputChanged(val, index); };
|
||||
|
||||
var sliderObj = UIFactory.CreateSlider(row, "Slider", out Slider slider);
|
||||
m_sliders[index] = slider;
|
||||
UIFactory.SetLayoutElement(sliderObj, minHeight: 25, minWidth: 70, flexibleWidth: 999, flexibleHeight: 0);
|
||||
slider.minValue = 0;
|
||||
slider.maxValue = 1;
|
||||
slider.onValueChanged.AddListener((float val) => { OnSliderValueChanged(val, index); });
|
||||
}
|
||||
}
|
||||
}
|
249
src/CacheObject/IValues/InteractiveDictionary.cs
Normal file
249
src/CacheObject/IValues/InteractiveDictionary.cs
Normal file
@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveDictionary : InteractiveValue, ICellPoolDataSource<CacheKeyValuePairCell>, ICacheObjectController
|
||||
{
|
||||
CacheObjectBase ICacheObjectController.ParentCacheObject => this.CurrentOwner;
|
||||
object ICacheObjectController.Target => this.CurrentOwner.Value;
|
||||
public Type TargetType { get; private set; }
|
||||
|
||||
public override bool CanWrite => base.CanWrite && RefIDictionary != null && !RefIDictionary.IsReadOnly;
|
||||
|
||||
public Type KeysType;
|
||||
public Type ValuesType;
|
||||
public IDictionary RefIDictionary;
|
||||
|
||||
public int ItemCount => cachedEntries.Count;
|
||||
private readonly List<CacheKeyValuePair> cachedEntries = new List<CacheKeyValuePair>();
|
||||
|
||||
public ScrollPool<CacheKeyValuePairCell> DictScrollPool { get; private set; }
|
||||
|
||||
private Text NotSupportedLabel;
|
||||
|
||||
public Text TopLabel;
|
||||
|
||||
public LayoutElement KeyTitleLayout;
|
||||
public LayoutElement ValueTitleLayout;
|
||||
|
||||
public override void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
base.OnBorrowed(owner);
|
||||
|
||||
DictScrollPool.Refresh(true, true);
|
||||
}
|
||||
|
||||
public override void ReleaseFromOwner()
|
||||
{
|
||||
base.ReleaseFromOwner();
|
||||
|
||||
ClearAndRelease();
|
||||
}
|
||||
|
||||
private void ClearAndRelease()
|
||||
{
|
||||
RefIDictionary = null;
|
||||
|
||||
foreach (var entry in cachedEntries)
|
||||
{
|
||||
entry.UnlinkFromView();
|
||||
entry.ReleasePooledObjects();
|
||||
}
|
||||
|
||||
cachedEntries.Clear();
|
||||
}
|
||||
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// should never be null
|
||||
ClearAndRelease();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var type = value.GetActualType();
|
||||
ReflectionUtility.TryGetEntryTypes(type, out KeysType, out ValuesType);
|
||||
|
||||
CacheEntries(value);
|
||||
|
||||
TopLabel.text = $"[{cachedEntries.Count}] {SignatureHighlighter.Parse(type, false)}";
|
||||
}
|
||||
|
||||
this.DictScrollPool.Refresh(true, false);
|
||||
}
|
||||
|
||||
private void CacheEntries(object value)
|
||||
{
|
||||
RefIDictionary = value as IDictionary;
|
||||
|
||||
if (ReflectionUtility.TryGetDictEnumerator(value, out IEnumerator<DictionaryEntry> dictEnumerator))
|
||||
{
|
||||
NotSupportedLabel.gameObject.SetActive(false);
|
||||
|
||||
int idx = 0;
|
||||
while (dictEnumerator.MoveNext())
|
||||
{
|
||||
CacheKeyValuePair cache;
|
||||
if (idx >= cachedEntries.Count)
|
||||
{
|
||||
cache = new CacheKeyValuePair();
|
||||
cache.SetDictOwner(this, idx);
|
||||
cachedEntries.Add(cache);
|
||||
}
|
||||
else
|
||||
cache = cachedEntries[idx];
|
||||
|
||||
cache.SetFallbackType(ValuesType);
|
||||
cache.SetKey(dictEnumerator.Current.Key);
|
||||
cache.SetValueFromSource(dictEnumerator.Current.Value);
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Remove excess cached entries if dict count decreased
|
||||
if (cachedEntries.Count > idx)
|
||||
{
|
||||
for (int i = cachedEntries.Count - 1; i >= idx; i--)
|
||||
{
|
||||
var cache = cachedEntries[i];
|
||||
if (cache.CellView != null)
|
||||
cache.UnlinkFromView();
|
||||
|
||||
cache.ReleasePooledObjects();
|
||||
cachedEntries.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NotSupportedLabel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Setting value to dictionary
|
||||
|
||||
public void TrySetValueToKey(object key, object value, int keyIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!RefIDictionary.Contains(key))
|
||||
{
|
||||
ExplorerCore.LogWarning("Unable to set key! Key may have been boxed to/from Il2Cpp Object.");
|
||||
return;
|
||||
}
|
||||
|
||||
RefIDictionary[key] = value;
|
||||
|
||||
var entry = cachedEntries[keyIndex];
|
||||
entry.SetValueFromSource(value);
|
||||
if (entry.CellView != null)
|
||||
entry.SetDataToCell(entry.CellView);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting IDictionary key! {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// KVP entry scroll pool
|
||||
|
||||
public void OnCellBorrowed(CacheKeyValuePairCell cell) { }
|
||||
|
||||
public void SetCell(CacheKeyValuePairCell cell, int index)
|
||||
{
|
||||
CacheObjectControllerHelper.SetCell(cell, index, cachedEntries, SetCellLayout);
|
||||
}
|
||||
|
||||
public int AdjustedWidth => (int)UIRect.rect.width - 80;
|
||||
|
||||
public override void SetLayout()
|
||||
{
|
||||
var minHeight = 5f;
|
||||
|
||||
KeyTitleLayout.minWidth = AdjustedWidth * 0.44f;
|
||||
ValueTitleLayout.minWidth = AdjustedWidth * 0.55f;
|
||||
|
||||
foreach (var cell in DictScrollPool.CellPool)
|
||||
{
|
||||
SetCellLayout(cell);
|
||||
if (cell.Enabled)
|
||||
minHeight += cell.Rect.rect.height;
|
||||
}
|
||||
|
||||
this.scrollLayout.minHeight = Math.Min(InspectorPanel.CurrentPanelHeight - 400f, minHeight);
|
||||
}
|
||||
|
||||
private void SetCellLayout(CacheObjectCell objcell)
|
||||
{
|
||||
var cell = objcell as CacheKeyValuePairCell;
|
||||
cell.KeyGroupLayout.minWidth = cell.AdjustedWidth * 0.44f;
|
||||
cell.RightGroupLayout.minWidth = cell.AdjustedWidth * 0.55f;
|
||||
|
||||
if (cell.Occupant?.IValue != null)
|
||||
cell.Occupant.IValue.SetLayout();
|
||||
}
|
||||
|
||||
private LayoutElement scrollLayout;
|
||||
private RectTransform UIRect;
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveDict", true, true, true, true, 6, new Vector4(10, 3, 15, 4),
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
UIFactory.SetLayoutElement(UIRoot, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 475);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
UIRect = UIRoot.GetComponent<RectTransform>();
|
||||
|
||||
// Entries label
|
||||
|
||||
TopLabel = UIFactory.CreateLabel(UIRoot, "EntryLabel", "not set", TextAnchor.MiddleLeft, fontSize: 16);
|
||||
TopLabel.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
|
||||
// key / value titles
|
||||
|
||||
var titleGroup = UIFactory.CreateUIObject("TitleGroup", UIRoot);
|
||||
UIFactory.SetLayoutElement(titleGroup, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 0);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(titleGroup, false, true, true, true, padLeft: 65, padRight: 0, childAlignment: TextAnchor.LowerLeft);
|
||||
|
||||
var keyTitle = UIFactory.CreateLabel(titleGroup, "KeyTitle", "Keys", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(keyTitle.gameObject, minWidth: 100, flexibleWidth: 0);
|
||||
KeyTitleLayout = keyTitle.GetComponent<LayoutElement>();
|
||||
|
||||
var valueTitle = UIFactory.CreateLabel(titleGroup, "ValueTitle", "Values", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(valueTitle.gameObject, minWidth: 100, flexibleWidth: 0);
|
||||
ValueTitleLayout = valueTitle.GetComponent<LayoutElement>();
|
||||
|
||||
// entry scroll pool
|
||||
|
||||
DictScrollPool = UIFactory.CreateScrollPool<CacheKeyValuePairCell>(UIRoot, "EntryList", out GameObject scrollObj,
|
||||
out GameObject _, new Color(0.09f, 0.09f, 0.09f));
|
||||
UIFactory.SetLayoutElement(scrollObj, minHeight: 150, flexibleHeight: 0);
|
||||
DictScrollPool.Initialize(this, SetLayout);
|
||||
scrollLayout = scrollObj.GetComponent<LayoutElement>();
|
||||
|
||||
NotSupportedLabel = UIFactory.CreateLabel(DictScrollPool.Content.gameObject, "NotSupportedMessage",
|
||||
"The IDictionary failed to enumerate. This is likely due to an issue with Unhollowed interfaces.",
|
||||
TextAnchor.MiddleLeft, Color.red);
|
||||
|
||||
UIFactory.SetLayoutElement(NotSupportedLabel.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||
NotSupportedLabel.gameObject.SetActive(false);
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
256
src/CacheObject/IValues/InteractiveEnum.cs
Normal file
256
src/CacheObject/IValues/InteractiveEnum.cs
Normal file
@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveEnum : InteractiveValue
|
||||
{
|
||||
public bool IsFlags;
|
||||
public Type EnumType;
|
||||
|
||||
private Type lastType;
|
||||
|
||||
public OrderedDictionary CurrentValues;
|
||||
|
||||
public CachedEnumValue ValueAtIdx(int idx) => (CachedEnumValue)CurrentValues[idx];
|
||||
public CachedEnumValue ValueAtKey(object key) => (CachedEnumValue)CurrentValues[key];
|
||||
|
||||
private Dropdown enumDropdown;
|
||||
private GameObject toggleHolder;
|
||||
private readonly List<Toggle> flagToggles = new List<Toggle>();
|
||||
private readonly List<Text> flagTexts = new List<Text>();
|
||||
|
||||
// Setting value from owner
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
EnumType = value.GetType();
|
||||
|
||||
if (lastType != EnumType)
|
||||
{
|
||||
CurrentValues = GetEnumValues(EnumType, out IsFlags);
|
||||
|
||||
if (IsFlags)
|
||||
SetupTogglesForEnumType();
|
||||
else
|
||||
SetupDropdownForEnumType();
|
||||
|
||||
lastType = EnumType;
|
||||
}
|
||||
|
||||
// setup ui for changes
|
||||
if (IsFlags)
|
||||
SetTogglesForValue(value);
|
||||
else
|
||||
SetDropdownForValue(value);
|
||||
}
|
||||
|
||||
// Setting value to owner
|
||||
|
||||
private void OnApplyClicked()
|
||||
{
|
||||
if (IsFlags)
|
||||
SetValueFromFlags();
|
||||
else
|
||||
SetValueFromDropdown();
|
||||
}
|
||||
|
||||
private void SetValueFromDropdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
CurrentOwner.SetUserValue(ValueAtIdx(enumDropdown.value).ActualValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception setting from dropdown: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetValueFromFlags()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> values = new List<string>();
|
||||
for (int i = 0; i < CurrentValues.Count; i++)
|
||||
{
|
||||
if (flagToggles[i].isOn)
|
||||
values.Add(ValueAtIdx(i).Name);
|
||||
}
|
||||
|
||||
CurrentOwner.SetUserValue(Enum.Parse(EnumType, string.Join(", ", values.ToArray())));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception setting from flag toggles: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
// setting UI state for value
|
||||
|
||||
private void SetDropdownForValue(object value)
|
||||
{
|
||||
if (CurrentValues.Contains(value))
|
||||
{
|
||||
var cached = ValueAtKey(value);
|
||||
enumDropdown.value = cached.EnumIndex;
|
||||
enumDropdown.RefreshShownValue();
|
||||
}
|
||||
else
|
||||
ExplorerCore.LogWarning("CurrentValues does not contain key '" + value?.ToString() ?? "<null>" + "'");
|
||||
}
|
||||
|
||||
private void SetTogglesForValue(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var split = value.ToString().Split(',');
|
||||
var set = new HashSet<string>();
|
||||
foreach (var s in split)
|
||||
set.Add(s.Trim());
|
||||
|
||||
for (int i = 0; i < CurrentValues.Count; i++)
|
||||
flagToggles[i].isOn = set.Contains(ValueAtIdx(i).Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception setting flag toggles: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Setting up the UI for the enum type when it changes or is first set
|
||||
|
||||
private void SetupDropdownForEnumType()
|
||||
{
|
||||
toggleHolder.SetActive(false);
|
||||
enumDropdown.gameObject.SetActive(true);
|
||||
|
||||
// create dropdown entries
|
||||
enumDropdown.options.Clear();
|
||||
|
||||
foreach (CachedEnumValue entry in CurrentValues.Values)
|
||||
enumDropdown.options.Add(new Dropdown.OptionData(entry.Name));
|
||||
|
||||
enumDropdown.value = 0;
|
||||
enumDropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
private void SetupTogglesForEnumType()
|
||||
{
|
||||
toggleHolder.SetActive(true);
|
||||
enumDropdown.gameObject.SetActive(false);
|
||||
|
||||
// create / set / hide toggles
|
||||
for (int i = 0; i < CurrentValues.Count || i < flagToggles.Count; i++)
|
||||
{
|
||||
if (i >= CurrentValues.Count)
|
||||
{
|
||||
if (i >= flagToggles.Count)
|
||||
break;
|
||||
|
||||
flagToggles[i].gameObject.SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i >= flagToggles.Count)
|
||||
AddToggleRow();
|
||||
|
||||
flagToggles[i].isOn = false;
|
||||
flagTexts[i].text = ValueAtIdx(i).Name;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToggleRow()
|
||||
{
|
||||
var row = UIFactory.CreateUIObject("ToggleRow", toggleHolder);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(row, false, false, true, true, 2);
|
||||
UIFactory.SetLayoutElement(row, minHeight: 25, flexibleWidth: 9999);
|
||||
|
||||
var toggleObj = UIFactory.CreateToggle(row, "ToggleObj", out Toggle toggle, out Text toggleText);
|
||||
UIFactory.SetLayoutElement(toggleObj, minHeight: 25, flexibleWidth: 9999);
|
||||
|
||||
flagToggles.Add(toggle);
|
||||
flagTexts.Add(toggleText);
|
||||
}
|
||||
|
||||
// UI Construction
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveEnum", false, false, true, true, 3, new Vector4(4, 4, 4, 4),
|
||||
new Color(0.06f, 0.06f, 0.06f));
|
||||
UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleHeight: 9999, flexibleWidth: 9999);
|
||||
|
||||
var hori = UIFactory.CreateUIObject("Hori", UIRoot);
|
||||
UIFactory.SetLayoutElement(hori, minHeight: 25, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(hori, false, false, true, true, 2);
|
||||
|
||||
var applyButton = UIFactory.CreateButton(hori, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f));
|
||||
UIFactory.SetLayoutElement(applyButton.Component.gameObject, minHeight: 25, minWidth: 100);
|
||||
applyButton.OnClick += OnApplyClicked;
|
||||
|
||||
var dropdownObj = UIFactory.CreateDropdown(hori, out enumDropdown, "not set", 14, null);
|
||||
UIFactory.SetLayoutElement(dropdownObj, minHeight: 25, flexibleWidth: 600);
|
||||
|
||||
toggleHolder = UIFactory.CreateUIObject("ToggleHolder", UIRoot);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(toggleHolder, false, false, true, true, 4);
|
||||
UIFactory.SetLayoutElement(toggleHolder, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 9999);
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
|
||||
|
||||
#region Enum cache
|
||||
|
||||
public struct CachedEnumValue
|
||||
{
|
||||
public CachedEnumValue(object value, int index, string name)
|
||||
{
|
||||
EnumIndex = index;
|
||||
Name = name;
|
||||
ActualValue = value;
|
||||
}
|
||||
|
||||
public readonly object ActualValue;
|
||||
public int EnumIndex;
|
||||
public readonly string Name;
|
||||
}
|
||||
|
||||
internal static readonly Dictionary<string, OrderedDictionary> enumCache = new Dictionary<string, OrderedDictionary>();
|
||||
|
||||
internal static OrderedDictionary GetEnumValues(Type enumType, out bool isFlags)
|
||||
{
|
||||
isFlags = enumType.GetCustomAttributes(typeof(FlagsAttribute), true) is object[] fa && fa.Any();
|
||||
|
||||
if (!enumCache.ContainsKey(enumType.AssemblyQualifiedName))
|
||||
{
|
||||
var dict = new OrderedDictionary();
|
||||
var addedNames = new HashSet<string>();
|
||||
|
||||
int i = 0;
|
||||
foreach (var value in Enum.GetValues(enumType))
|
||||
{
|
||||
var name = value.ToString();
|
||||
if (addedNames.Contains(name))
|
||||
continue;
|
||||
addedNames.Add(name);
|
||||
|
||||
dict.Add(value, new CachedEnumValue(value, i, name));
|
||||
i++;
|
||||
}
|
||||
|
||||
enumCache.Add(enumType.AssemblyQualifiedName, dict);
|
||||
}
|
||||
|
||||
return enumCache[enumType.AssemblyQualifiedName];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
268
src/CacheObject/IValues/InteractiveList.cs
Normal file
268
src/CacheObject/IValues/InteractiveList.cs
Normal file
@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.CacheObject.Views;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Panels;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveList : InteractiveValue, ICellPoolDataSource<CacheListEntryCell>, ICacheObjectController
|
||||
{
|
||||
CacheObjectBase ICacheObjectController.ParentCacheObject => this.CurrentOwner;
|
||||
object ICacheObjectController.Target => this.CurrentOwner.Value;
|
||||
public Type TargetType { get; private set; }
|
||||
|
||||
public override bool CanWrite => base.CanWrite && ((RefIList != null && !RefIList.IsReadOnly) || IsWritableGenericIList);
|
||||
|
||||
public Type EntryType;
|
||||
public IList RefIList;
|
||||
|
||||
private bool IsWritableGenericIList;
|
||||
private PropertyInfo genericIndexer;
|
||||
|
||||
public int ItemCount => cachedEntries.Count;
|
||||
private readonly List<CacheListEntry> cachedEntries = new List<CacheListEntry>();
|
||||
|
||||
public ScrollPool<CacheListEntryCell> ListScrollPool { get; private set; }
|
||||
|
||||
public Text TopLabel;
|
||||
|
||||
public override void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
base.OnBorrowed(owner);
|
||||
|
||||
ListScrollPool.Refresh(true, true);
|
||||
}
|
||||
|
||||
public override void ReleaseFromOwner()
|
||||
{
|
||||
base.ReleaseFromOwner();
|
||||
|
||||
ClearAndRelease();
|
||||
}
|
||||
|
||||
private void ClearAndRelease()
|
||||
{
|
||||
RefIList = null;
|
||||
|
||||
foreach (var entry in cachedEntries)
|
||||
{
|
||||
entry.UnlinkFromView();
|
||||
entry.ReleasePooledObjects();
|
||||
}
|
||||
|
||||
cachedEntries.Clear();
|
||||
}
|
||||
|
||||
// Setting the List value itself to this model
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// should never be null
|
||||
if (cachedEntries.Any())
|
||||
ClearAndRelease();
|
||||
}
|
||||
else
|
||||
{
|
||||
var type = value.GetActualType();
|
||||
ReflectionUtility.TryGetEntryType(type, out EntryType);
|
||||
|
||||
CacheEntries(value);
|
||||
|
||||
TopLabel.text = $"[{cachedEntries.Count}] {SignatureHighlighter.Parse(type, false)}";
|
||||
}
|
||||
|
||||
//this.ScrollPoolLayout.minHeight = Math.Min(400f, 35f * values.Count);
|
||||
this.ListScrollPool.Refresh(true, false);
|
||||
}
|
||||
|
||||
private void CacheEntries(object value)
|
||||
{
|
||||
RefIList = value as IList;
|
||||
|
||||
// Check if the type implements IList<T> but not IList (ie. Il2CppArrayBase)
|
||||
if (RefIList == null)
|
||||
CheckGenericIList(value);
|
||||
else
|
||||
IsWritableGenericIList = false;
|
||||
|
||||
int idx = 0;
|
||||
|
||||
if (ReflectionUtility.TryGetEnumerator(value, out IEnumerator enumerator))
|
||||
{
|
||||
NotSupportedLabel.gameObject.SetActive(false);
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var entry = enumerator.Current;
|
||||
|
||||
// If list count increased, create new cache entries
|
||||
CacheListEntry cache;
|
||||
if (idx >= cachedEntries.Count)
|
||||
{
|
||||
cache = new CacheListEntry();
|
||||
cache.SetListOwner(this, idx);
|
||||
cachedEntries.Add(cache);
|
||||
}
|
||||
else
|
||||
cache = cachedEntries[idx];
|
||||
|
||||
cache.SetFallbackType(this.EntryType);
|
||||
cache.SetValueFromSource(entry);
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Remove excess cached entries if list count decreased
|
||||
if (cachedEntries.Count > idx)
|
||||
{
|
||||
for (int i = cachedEntries.Count - 1; i >= idx; i--)
|
||||
{
|
||||
var cache = cachedEntries[i];
|
||||
if (cache.CellView != null)
|
||||
cache.UnlinkFromView();
|
||||
|
||||
cache.ReleasePooledObjects();
|
||||
cachedEntries.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NotSupportedLabel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckGenericIList(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = value.GetType();
|
||||
if (type.GetInterfaces().Any(it => it.IsGenericType && it.GetGenericTypeDefinition() == typeof(IList<>)))
|
||||
IsWritableGenericIList = !(bool)type.GetProperty("IsReadOnly").GetValue(value, null);
|
||||
else
|
||||
IsWritableGenericIList = false;
|
||||
|
||||
if (IsWritableGenericIList)
|
||||
{
|
||||
// Find the "this[int index]" property.
|
||||
// It might be a private implementation.
|
||||
foreach (var prop in type.GetProperties(ReflectionUtility.FLAGS))
|
||||
{
|
||||
if ((prop.Name == "Item"
|
||||
|| (prop.Name.StartsWith("System.Collections.Generic.IList<") && prop.Name.EndsWith(">.Item")))
|
||||
&& prop.GetIndexParameters() is ParameterInfo[] parameters
|
||||
&& parameters.Length == 1
|
||||
&& parameters[0].ParameterType == typeof(int))
|
||||
{
|
||||
genericIndexer = prop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (genericIndexer == null)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Failed to find indexer property for IList<T> type '{type.FullName}'!");
|
||||
IsWritableGenericIList = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception processing IEnumerable for IList<T> check: {ex.ReflectionExToString()}");
|
||||
IsWritableGenericIList = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting the value of an index to the list
|
||||
|
||||
public void TrySetValueToIndex(object value, int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsWritableGenericIList)
|
||||
{
|
||||
RefIList[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
genericIndexer.SetValue(CurrentOwner.Value, value, new object[] { index });
|
||||
}
|
||||
|
||||
var entry = cachedEntries[index];
|
||||
entry.SetValueFromSource(value);
|
||||
|
||||
if (entry.CellView != null)
|
||||
entry.SetDataToCell(entry.CellView);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting IList value: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// List entry scroll pool
|
||||
|
||||
public override void SetLayout()
|
||||
{
|
||||
var minHeight = 5f;
|
||||
|
||||
foreach (var cell in ListScrollPool.CellPool)
|
||||
{
|
||||
if (cell.Enabled)
|
||||
minHeight += cell.Rect.rect.height;
|
||||
}
|
||||
|
||||
this.scrollLayout.minHeight = Math.Min(InspectorPanel.CurrentPanelHeight - 400f, minHeight);
|
||||
}
|
||||
|
||||
public void OnCellBorrowed(CacheListEntryCell cell) { } // not needed
|
||||
|
||||
public void SetCell(CacheListEntryCell cell, int index)
|
||||
{
|
||||
CacheObjectControllerHelper.SetCell(cell, index, cachedEntries, null);
|
||||
}
|
||||
|
||||
private LayoutElement scrollLayout;
|
||||
|
||||
private Text NotSupportedLabel;
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveList", true, true, true, true, 6, new Vector4(10, 3, 15, 4),
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
UIFactory.SetLayoutElement(UIRoot, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 600);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// Entries label
|
||||
|
||||
TopLabel = UIFactory.CreateLabel(UIRoot, "EntryLabel", "not set", TextAnchor.MiddleLeft, fontSize: 16);
|
||||
TopLabel.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
|
||||
// entry scroll pool
|
||||
|
||||
ListScrollPool = UIFactory.CreateScrollPool<CacheListEntryCell>(UIRoot, "EntryList", out GameObject scrollObj,
|
||||
out GameObject _, new Color(0.09f, 0.09f, 0.09f));
|
||||
UIFactory.SetLayoutElement(scrollObj, minHeight: 400, flexibleHeight: 0);
|
||||
ListScrollPool.Initialize(this, SetLayout);
|
||||
scrollLayout = scrollObj.GetComponent<LayoutElement>();
|
||||
|
||||
NotSupportedLabel = UIFactory.CreateLabel(ListScrollPool.Content.gameObject, "NotSupportedMessage",
|
||||
"The IEnumerable failed to enumerate. This is likely due to an issue with Unhollowed interfaces.",
|
||||
TextAnchor.MiddleLeft, Color.red);
|
||||
|
||||
UIFactory.SetLayoutElement(NotSupportedLabel.gameObject, minHeight: 25, flexibleWidth: 9999);
|
||||
NotSupportedLabel.gameObject.SetActive(false);
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
131
src/CacheObject/IValues/InteractiveString.cs
Normal file
131
src/CacheObject/IValues/InteractiveString.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core.Config;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveString : InteractiveValue
|
||||
{
|
||||
private string RealValue;
|
||||
public string EditedValue = "";
|
||||
|
||||
public InputFieldRef inputField;
|
||||
public ButtonRef ApplyButton;
|
||||
|
||||
public GameObject SaveFileRow;
|
||||
public InputFieldRef SaveFilePath;
|
||||
|
||||
public override void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
base.OnBorrowed(owner);
|
||||
|
||||
inputField.Component.readOnly = !owner.CanWrite;
|
||||
ApplyButton.Component.gameObject.SetActive(owner.CanWrite);
|
||||
|
||||
SaveFilePath.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, "untitled.txt");
|
||||
}
|
||||
|
||||
private bool IsStringTooLong(string s)
|
||||
{
|
||||
if (s == null)
|
||||
return false;
|
||||
|
||||
return s.Length >= UIManager.MAX_INPUTFIELD_CHARS;
|
||||
}
|
||||
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
RealValue = value as string;
|
||||
SaveFileRow.SetActive(IsStringTooLong(RealValue));
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
inputField.Text = "";
|
||||
EditedValue = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
EditedValue = (string)value;
|
||||
inputField.Text = EditedValue;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplyClicked()
|
||||
{
|
||||
CurrentOwner.SetUserValue(EditedValue);
|
||||
}
|
||||
|
||||
private void OnInputChanged(string input)
|
||||
{
|
||||
EditedValue = input;
|
||||
SaveFileRow.SetActive(IsStringTooLong(EditedValue));
|
||||
}
|
||||
|
||||
private void OnSaveFileClicked()
|
||||
{
|
||||
if (RealValue == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(SaveFilePath.Text))
|
||||
{
|
||||
ExplorerCore.LogWarning("Cannot save an empty file path!");
|
||||
return;
|
||||
}
|
||||
|
||||
var path = IOUtility.EnsureValidDirectory(SaveFilePath.Text);
|
||||
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
|
||||
File.WriteAllText(path, RealValue);
|
||||
}
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveString", false, false, true, true, 3, new Vector4(4, 4, 4, 4),
|
||||
new Color(0.06f, 0.06f, 0.06f));
|
||||
|
||||
// Save to file helper
|
||||
|
||||
SaveFileRow = UIFactory.CreateUIObject("SaveFileRow", UIRoot);
|
||||
UIFactory.SetLayoutElement(SaveFileRow, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(SaveFileRow, false, true, true, true, 3);
|
||||
|
||||
UIFactory.CreateLabel(SaveFileRow, "Info", "<color=red>String is too long! Save to file if you want to see the full string.</color>",
|
||||
TextAnchor.MiddleLeft);
|
||||
|
||||
var horizRow = UIFactory.CreateUIObject("Horiz", SaveFileRow);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(horizRow, false, false, true, true, 4);
|
||||
|
||||
var saveButton = UIFactory.CreateButton(horizRow, "SaveButton", "Save file");
|
||||
UIFactory.SetLayoutElement(saveButton.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
|
||||
saveButton.OnClick += OnSaveFileClicked;
|
||||
|
||||
SaveFilePath = UIFactory.CreateInputField(horizRow, "SaveInput", "...");
|
||||
UIFactory.SetLayoutElement(SaveFilePath.UIRoot, minHeight: 25, flexibleWidth: 9999);
|
||||
|
||||
// Main Input / apply
|
||||
|
||||
ApplyButton = UIFactory.CreateButton(UIRoot, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f));
|
||||
UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0);
|
||||
ApplyButton.OnClick += OnApplyClicked;
|
||||
|
||||
inputField = UIFactory.CreateInputField(UIRoot, "InputField", "empty");
|
||||
inputField.UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
UIFactory.SetLayoutElement(inputField.UIRoot, minHeight: 25, flexibleHeight: 500, flexibleWidth: 9999);
|
||||
inputField.Component.lineType = InputField.LineType.MultiLineNewline;
|
||||
inputField.OnValueChanged += OnInputChanged;
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
70
src/CacheObject/IValues/InteractiveValue.cs
Normal file
70
src/CacheObject/IValues/InteractiveValue.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Models;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public abstract class InteractiveValue : IPooledObject
|
||||
{
|
||||
public static Type GetIValueTypeForState(ValueState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ValueState.String:
|
||||
return typeof(InteractiveString);
|
||||
case ValueState.Enum:
|
||||
return typeof(InteractiveEnum);
|
||||
case ValueState.Collection:
|
||||
return typeof(InteractiveList);
|
||||
case ValueState.Dictionary:
|
||||
return typeof(InteractiveDictionary);
|
||||
case ValueState.ValueStruct:
|
||||
return typeof(InteractiveValueStruct);
|
||||
case ValueState.Color:
|
||||
return typeof(InteractiveColor);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject UIRoot { get; set; }
|
||||
public float DefaultHeight => -1f;
|
||||
|
||||
public virtual bool CanWrite => this.CurrentOwner.CanWrite;
|
||||
|
||||
public CacheObjectBase CurrentOwner => m_owner;
|
||||
private CacheObjectBase m_owner;
|
||||
|
||||
public bool PendingValueWanted;
|
||||
|
||||
public virtual void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
if (this.m_owner != null)
|
||||
{
|
||||
ExplorerCore.LogWarning("Setting an IValue's owner but there is already one set. Maybe it wasn't cleaned up?");
|
||||
ReleaseFromOwner();
|
||||
}
|
||||
|
||||
this.m_owner = owner;
|
||||
}
|
||||
|
||||
public virtual void ReleaseFromOwner()
|
||||
{
|
||||
if (this.m_owner == null)
|
||||
return;
|
||||
|
||||
this.m_owner = null;
|
||||
}
|
||||
|
||||
public abstract void SetValue(object value);
|
||||
|
||||
public virtual void SetLayout() { }
|
||||
|
||||
public abstract GameObject CreateContent(GameObject parent);
|
||||
}
|
||||
}
|
212
src/CacheObject/IValues/InteractiveValueStruct.cs
Normal file
212
src/CacheObject/IValues/InteractiveValueStruct.cs
Normal file
@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject.IValues
|
||||
{
|
||||
public class InteractiveValueStruct : InteractiveValue
|
||||
{
|
||||
#region Struct cache / wrapper
|
||||
|
||||
public class StructInfo
|
||||
{
|
||||
public bool IsSupported;
|
||||
public FieldInfo[] Fields;
|
||||
|
||||
public StructInfo(bool isSupported, FieldInfo[] fields)
|
||||
{
|
||||
IsSupported = isSupported;
|
||||
Fields = fields;
|
||||
}
|
||||
|
||||
public void SetValue(object instance, string input, int fieldIndex)
|
||||
{
|
||||
var field = Fields[fieldIndex];
|
||||
|
||||
object val;
|
||||
if (field.FieldType == typeof(string))
|
||||
val = input;
|
||||
else
|
||||
{
|
||||
if (!ParseUtility.TryParse(input, field.FieldType, out val, out Exception ex))
|
||||
{
|
||||
ExplorerCore.LogWarning("Unable to parse input!");
|
||||
if (ex != null) ExplorerCore.Log(ex.ReflectionExToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
field.SetValue(instance, val);
|
||||
}
|
||||
|
||||
public string GetValue(object instance, int fieldIndex)
|
||||
{
|
||||
var field = Fields[fieldIndex];
|
||||
var value = field.GetValue(instance);
|
||||
return ParseUtility.ToStringForInput(value, field.FieldType);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, StructInfo> typeSupportCache = new Dictionary<string, StructInfo>();
|
||||
|
||||
private const BindingFlags INSTANCE_FLAGS = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
|
||||
private const string SYSTEM_VOID = "System.Void";
|
||||
|
||||
public static bool SupportsType(Type type)
|
||||
{
|
||||
if (!type.IsValueType || string.IsNullOrEmpty(type.AssemblyQualifiedName) || type.FullName == SYSTEM_VOID)
|
||||
return false;
|
||||
|
||||
if (typeSupportCache.TryGetValue(type.AssemblyQualifiedName, out var info))
|
||||
return info.IsSupported;
|
||||
|
||||
var supported = false;
|
||||
|
||||
var fields = type.GetFields(INSTANCE_FLAGS);
|
||||
if (fields.Length > 0)
|
||||
{
|
||||
if (fields.Any(it => !ParseUtility.CanParse(it.FieldType)))
|
||||
{
|
||||
supported = false;
|
||||
info = new StructInfo(supported, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
supported = true;
|
||||
info = new StructInfo(supported, fields);
|
||||
}
|
||||
}
|
||||
|
||||
typeSupportCache.Add(type.AssemblyQualifiedName, info);
|
||||
|
||||
return supported;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public object RefInstance;
|
||||
|
||||
public StructInfo CurrentInfo;
|
||||
private Type lastStructType;
|
||||
|
||||
private ButtonRef applyButton;
|
||||
private readonly List<GameObject> fieldRows = new List<GameObject>();
|
||||
private readonly List<InputFieldRef> inputFields = new List<InputFieldRef>();
|
||||
private readonly List<Text> labels = new List<Text>();
|
||||
|
||||
public override void OnBorrowed(CacheObjectBase owner)
|
||||
{
|
||||
base.OnBorrowed(owner);
|
||||
|
||||
applyButton.Component.gameObject.SetActive(owner.CanWrite);
|
||||
}
|
||||
|
||||
// Setting value from owner to this
|
||||
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
RefInstance = value;
|
||||
|
||||
var type = RefInstance.GetType();
|
||||
|
||||
if (type != lastStructType)
|
||||
{
|
||||
CurrentInfo = typeSupportCache[type.AssemblyQualifiedName];
|
||||
SetupUIForType();
|
||||
lastStructType = type;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CurrentInfo.Fields.Length; i++)
|
||||
{
|
||||
inputFields[i].Text = CurrentInfo.GetValue(RefInstance, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplyClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < CurrentInfo.Fields.Length; i++)
|
||||
{
|
||||
CurrentInfo.SetValue(RefInstance, inputFields[i].Text, i);
|
||||
}
|
||||
|
||||
CurrentOwner.SetUserValue(RefInstance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception setting value: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
// UI Setup for type
|
||||
|
||||
private void SetupUIForType()
|
||||
{
|
||||
for (int i = 0; i < CurrentInfo.Fields.Length || i <= inputFields.Count; i++)
|
||||
{
|
||||
if (i >= CurrentInfo.Fields.Length)
|
||||
{
|
||||
if (i >= inputFields.Count)
|
||||
break;
|
||||
|
||||
fieldRows[i].SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i >= inputFields.Count)
|
||||
AddEditorRow();
|
||||
|
||||
fieldRows[i].SetActive(true);
|
||||
|
||||
string label = SignatureHighlighter.Parse(CurrentInfo.Fields[i].FieldType, false);
|
||||
label += $" <color={SignatureHighlighter.FIELD_INSTANCE}>{CurrentInfo.Fields[i].Name}</color>:";
|
||||
labels[i].text = label;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEditorRow()
|
||||
{
|
||||
var row = UIFactory.CreateUIObject("HoriGroup", UIRoot);
|
||||
//row.AddComponent<ContentSizeFitter>().horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
UIFactory.SetLayoutElement(row, minHeight: 25, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(row, false, false, true, true, 8, childAlignment: TextAnchor.MiddleLeft);
|
||||
|
||||
fieldRows.Add(row);
|
||||
|
||||
var label = UIFactory.CreateLabel(row, "Label", "notset", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(label.gameObject, minHeight: 25, minWidth: 50, flexibleWidth: 0);
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
labels.Add(label);
|
||||
|
||||
var input = UIFactory.CreateInputField(row, "InputField", "...");
|
||||
UIFactory.SetLayoutElement(input.UIRoot, minHeight: 25, minWidth: 200);
|
||||
var fitter = input.UIRoot.AddComponent<ContentSizeFitter>();
|
||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
input.Component.lineType = InputField.LineType.MultiLineNewline;
|
||||
inputFields.Add(input);
|
||||
}
|
||||
|
||||
// UI Construction
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "InteractiveValueStruct", false, false, true, true, 3, new Vector4(4, 4, 4, 4),
|
||||
new Color(0.06f, 0.06f, 0.06f), TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleWidth: 9999);
|
||||
|
||||
applyButton = UIFactory.CreateButton(UIRoot, "ApplyButton", "Apply", new Color(0.2f, 0.27f, 0.2f));
|
||||
UIFactory.SetLayoutElement(applyButton.Component.gameObject, minHeight: 25, minWidth: 175);
|
||||
applyButton.OnClick += OnApplyClicked;
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
86
src/CacheObject/Views/CacheConfigCell.cs
Normal file
86
src/CacheObject/Views/CacheConfigCell.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public class ConfigEntryCell : CacheObjectCell
|
||||
{
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
// Main layout
|
||||
|
||||
UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30));
|
||||
Rect = UIRoot.GetComponent<RectTransform>();
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(UIRoot, false, false, true, true, 4, 4, 4, 4, 4, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// Left label
|
||||
|
||||
NameLabel = UIFactory.CreateLabel(UIRoot, "NameLabel", "<notset>", TextAnchor.MiddleLeft);
|
||||
NameLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UIFactory.SetLayoutElement(NameLabel.gameObject, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 300);
|
||||
NameLayout = NameLabel.GetComponent<LayoutElement>();
|
||||
|
||||
// horizontal group
|
||||
|
||||
var horiGroup = UIFactory.CreateUIObject("RightHoriGroup", UIRoot);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(horiGroup, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(horiGroup, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800);
|
||||
|
||||
SubContentButton = UIFactory.CreateButton(horiGroup, "SubContentButton", "▲", subInactiveColor);
|
||||
UIFactory.SetLayoutElement(SubContentButton.Component.gameObject, minWidth: 25, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
SubContentButton.OnClick += SubContentClicked;
|
||||
|
||||
// Type label
|
||||
|
||||
TypeLabel = UIFactory.CreateLabel(horiGroup, "TypeLabel", "<notset>", TextAnchor.MiddleLeft);
|
||||
TypeLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UIFactory.SetLayoutElement(TypeLabel.gameObject, minHeight: 25, flexibleHeight: 150, minWidth: 60, flexibleWidth: 0);
|
||||
|
||||
// Bool and number value interaction
|
||||
|
||||
var toggleObj = UIFactory.CreateToggle(horiGroup, "Toggle", out Toggle, out ToggleText);
|
||||
UIFactory.SetLayoutElement(toggleObj, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
ToggleText.color = SignatureHighlighter.KeywordBlue;
|
||||
Toggle.onValueChanged.AddListener(ToggleClicked);
|
||||
|
||||
InputField = UIFactory.CreateInputField(horiGroup, "InputField", "...");
|
||||
UIFactory.SetLayoutElement(InputField.UIRoot, minWidth: 150, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0);
|
||||
|
||||
// Apply
|
||||
|
||||
ApplyButton = UIFactory.CreateButton(horiGroup, "ApplyButton", "Apply", new Color(0.15f, 0.19f, 0.15f));
|
||||
UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
ApplyButton.OnClick += ApplyClicked;
|
||||
|
||||
// Main value label
|
||||
|
||||
ValueLabel = UIFactory.CreateLabel(horiGroup, "ValueLabel", "Value goes here", TextAnchor.MiddleLeft);
|
||||
ValueLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UIFactory.SetLayoutElement(ValueLabel.gameObject, minHeight: 25, flexibleHeight: 150, flexibleWidth: 9999);
|
||||
|
||||
// Subcontent
|
||||
|
||||
SubContentHolder = UIFactory.CreateUIObject("SubContent", UIRoot);
|
||||
UIFactory.SetLayoutElement(SubContentHolder.gameObject, minHeight: 30, flexibleHeight: 600, minWidth: 100, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(SubContentHolder, true, true, true, true, 2, childAlignment: TextAnchor.UpperLeft);
|
||||
//SubContentHolder.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.MinSize;
|
||||
SubContentHolder.SetActive(false);
|
||||
|
||||
// Bottom separator
|
||||
var separator = UIFactory.CreateUIObject("BottomSeperator", UIRoot);
|
||||
UIFactory.SetLayoutElement(separator, minHeight: 1, flexibleHeight: 0, flexibleWidth: 9999);
|
||||
separator.AddComponent<Image>().color = Color.black;
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
|
||||
protected override void ConstructEvaluateHolder(GameObject parent) { }
|
||||
}
|
||||
}
|
92
src/CacheObject/Views/CacheKeyValuePairCell.cs
Normal file
92
src/CacheObject/Views/CacheKeyValuePairCell.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public class CacheKeyValuePairCell : CacheObjectCell
|
||||
{
|
||||
public Image Image { get; private set; }
|
||||
public InteractiveDictionary DictOwner => Occupant.Owner as InteractiveDictionary;
|
||||
|
||||
public LayoutElement KeyGroupLayout;
|
||||
public Text KeyLabel;
|
||||
public ButtonRef KeyInspectButton;
|
||||
public InputFieldRef KeyInputField;
|
||||
public Text KeyInputTypeLabel;
|
||||
|
||||
public static Color EvenColor = new Color(0.07f, 0.07f, 0.07f);
|
||||
public static Color OddColor = new Color(0.063f, 0.063f, 0.063f);
|
||||
|
||||
public int AdjustedWidth => (int)Rect.rect.width - 70;
|
||||
|
||||
//public int HalfWidth => (int)(0.5f * Rect.rect.width) - 75;
|
||||
//public int AdjustedKeyWidth => HalfWidth - 50;
|
||||
//public int AdjustedRightWidth => HalfWidth;
|
||||
|
||||
private void KeyInspectClicked()
|
||||
{
|
||||
InspectorManager.Inspect((Occupant as CacheKeyValuePair).DictKey, this.Occupant);
|
||||
}
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
var root = base.CreateContent(parent);
|
||||
|
||||
Image = root.AddComponent<Image>();
|
||||
|
||||
this.NameLayout.minWidth = 70;
|
||||
this.NameLayout.flexibleWidth = 0;
|
||||
this.NameLayout.minHeight = 30;
|
||||
this.NameLayout.flexibleHeight = 0;
|
||||
this.NameLabel.alignment = TextAnchor.MiddleRight;
|
||||
|
||||
this.RightGroupLayout.minWidth = AdjustedWidth * 0.55f;
|
||||
|
||||
// Key area
|
||||
var keyGroup = UIFactory.CreateUIObject("KeyHolder", root.transform.Find("HoriGroup").gameObject);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(keyGroup, false, false, true, true, 2, 0, 0, 4, 4, childAlignment: TextAnchor.MiddleLeft);
|
||||
KeyGroupLayout = UIFactory.SetLayoutElement(keyGroup, minHeight: 30, minWidth: (int)(AdjustedWidth * 0.44f), flexibleWidth: 0);
|
||||
|
||||
// set to be after the NameLabel (our index label), and before the main horizontal group.
|
||||
keyGroup.transform.SetSiblingIndex(1);
|
||||
|
||||
// key Inspect
|
||||
|
||||
KeyInspectButton = UIFactory.CreateButton(keyGroup, "KeyInspectButton", "Inspect", new Color(0.15f, 0.15f, 0.15f));
|
||||
UIFactory.SetLayoutElement(KeyInspectButton.Component.gameObject, minWidth: 60, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0);
|
||||
KeyInspectButton.OnClick += KeyInspectClicked;
|
||||
|
||||
// label
|
||||
|
||||
KeyLabel = UIFactory.CreateLabel(keyGroup, "KeyLabel", "<i>empty</i>", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(KeyLabel.gameObject, minWidth: 50, flexibleWidth: 999, minHeight: 25);
|
||||
|
||||
// Type label for input field
|
||||
|
||||
KeyInputTypeLabel = UIFactory.CreateLabel(keyGroup, "InputTypeLabel", "<i>null</i>", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(KeyInputTypeLabel.gameObject, minWidth: 55, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0);
|
||||
|
||||
// input field
|
||||
|
||||
KeyInputField = UIFactory.CreateInputField(keyGroup, "KeyInput", "empty");
|
||||
UIFactory.SetLayoutElement(KeyInputField.UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 0, preferredWidth: 200);
|
||||
//KeyInputField.lineType = InputField.LineType.MultiLineNewline;
|
||||
KeyInputField.Component.readOnly = true;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override void ConstructEvaluateHolder(GameObject parent)
|
||||
{
|
||||
// not used
|
||||
}
|
||||
}
|
||||
}
|
44
src/CacheObject/Views/CacheListEntryCell.cs
Normal file
44
src/CacheObject/Views/CacheListEntryCell.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public class CacheListEntryCell : CacheObjectCell
|
||||
{
|
||||
public Image Image { get; private set; }
|
||||
public InteractiveList ListOwner => Occupant.Owner as InteractiveList;
|
||||
|
||||
public static Color EvenColor = new Color(0.12f, 0.12f, 0.12f);
|
||||
public static Color OddColor = new Color(0.1f, 0.1f, 0.1f);
|
||||
|
||||
public override GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
var root = base.CreateContent(parent);
|
||||
|
||||
Image = root.AddComponent<Image>();
|
||||
|
||||
this.NameLayout.minWidth = 40;
|
||||
this.NameLayout.flexibleWidth = 50;
|
||||
this.NameLayout.minHeight = 25;
|
||||
this.NameLayout.flexibleHeight = 0;
|
||||
this.NameLabel.alignment = TextAnchor.MiddleRight;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override void ConstructEvaluateHolder(GameObject parent)
|
||||
{
|
||||
// not used
|
||||
}
|
||||
|
||||
//protected override void ConstructUpdateToggle(GameObject parent)
|
||||
//{
|
||||
// // not used
|
||||
//}
|
||||
}
|
||||
}
|
52
src/CacheObject/Views/CacheMemberCell.cs
Normal file
52
src/CacheObject/Views/CacheMemberCell.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public class CacheMemberCell : CacheObjectCell
|
||||
{
|
||||
//public ReflectionInspector Owner { get; set; }
|
||||
|
||||
public CacheMember MemberOccupant => Occupant as CacheMember;
|
||||
|
||||
public GameObject EvaluateHolder;
|
||||
public ButtonRef EvaluateButton;
|
||||
|
||||
//public Toggle UpdateToggle;
|
||||
|
||||
protected virtual void EvaluateClicked()
|
||||
{
|
||||
this.MemberOccupant.OnEvaluateClicked();
|
||||
}
|
||||
|
||||
protected override void ConstructEvaluateHolder(GameObject parent)
|
||||
{
|
||||
// Evaluate vert group
|
||||
|
||||
EvaluateHolder = UIFactory.CreateUIObject("EvalGroup", parent);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(EvaluateHolder, false, false, true, true, 3);
|
||||
UIFactory.SetLayoutElement(EvaluateHolder, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 775);
|
||||
|
||||
EvaluateButton = UIFactory.CreateButton(EvaluateHolder, "EvaluateButton", "Evaluate", new Color(0.15f, 0.15f, 0.15f));
|
||||
UIFactory.SetLayoutElement(EvaluateButton.Component.gameObject, minWidth: 100, minHeight: 25);
|
||||
EvaluateButton.OnClick += EvaluateClicked;
|
||||
}
|
||||
|
||||
//protected override void ConstructUpdateToggle(GameObject parent)
|
||||
//{
|
||||
// // Auto-update toggle
|
||||
//
|
||||
// var updateToggle = UIFactory.CreateToggle(parent, "AutoUpdate", out UpdateToggle, out Text autoText);
|
||||
// UIFactory.SetLayoutElement(updateToggle, minHeight: 25, minWidth: 30, flexibleWidth: 0, flexibleHeight: 0);
|
||||
// GameObject.Destroy(autoText);
|
||||
// UpdateToggle.isOn = false;
|
||||
// UpdateToggle.onValueChanged.AddListener((bool val) => { MemberOccupant.AutoUpdateWanted = val; });
|
||||
//}
|
||||
}
|
||||
}
|
203
src/CacheObject/Views/CacheObjectCell.cs
Normal file
203
src/CacheObject/Views/CacheObjectCell.cs
Normal file
@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.CacheObject.IValues;
|
||||
using UnityExplorer.Inspectors;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Widgets;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public abstract class CacheObjectCell : ICell
|
||||
{
|
||||
#region ICell
|
||||
|
||||
public float DefaultHeight => 30f;
|
||||
|
||||
public GameObject UIRoot { get; set; }
|
||||
|
||||
public bool Enabled => m_enabled;
|
||||
private bool m_enabled;
|
||||
|
||||
public RectTransform Rect { get; set; }
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
m_enabled = false;
|
||||
UIRoot.SetActive(false);
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
m_enabled = true;
|
||||
UIRoot.SetActive(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public CacheObjectBase Occupant { get; set; }
|
||||
public bool SubContentActive => SubContentHolder.activeSelf;
|
||||
|
||||
public LayoutElement NameLayout;
|
||||
public LayoutElement RightGroupLayout;
|
||||
|
||||
public Text NameLabel;
|
||||
public InputFieldRef HiddenNameLabel;
|
||||
public Text TypeLabel;
|
||||
public Text ValueLabel;
|
||||
public Toggle Toggle;
|
||||
public Text ToggleText;
|
||||
public InputFieldRef InputField;
|
||||
|
||||
public ButtonRef InspectButton;
|
||||
public ButtonRef SubContentButton;
|
||||
public ButtonRef ApplyButton;
|
||||
|
||||
public GameObject SubContentHolder;
|
||||
|
||||
protected virtual void ApplyClicked()
|
||||
{
|
||||
Occupant.OnCellApplyClicked();
|
||||
}
|
||||
|
||||
protected virtual void InspectClicked()
|
||||
{
|
||||
InspectorManager.Inspect(Occupant.Value, this.Occupant);
|
||||
}
|
||||
|
||||
protected virtual void ToggleClicked(bool value)
|
||||
{
|
||||
ToggleText.text = value.ToString();
|
||||
}
|
||||
|
||||
protected virtual void SubContentClicked()
|
||||
{
|
||||
this.Occupant.OnCellSubContentToggle();
|
||||
}
|
||||
|
||||
public readonly Color subInactiveColor = new Color(0.23f, 0.23f, 0.23f);
|
||||
public readonly Color subActiveColor = new Color(0.23f, 0.33f, 0.23f);
|
||||
|
||||
public void RefreshSubcontentButton()
|
||||
{
|
||||
if (!this.SubContentHolder.activeSelf)
|
||||
{
|
||||
this.SubContentButton.ButtonText.text = "▲";
|
||||
RuntimeProvider.Instance.SetColorBlock(SubContentButton.Component, subInactiveColor, subInactiveColor * 1.3f);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SubContentButton.ButtonText.text = "▼";
|
||||
RuntimeProvider.Instance.SetColorBlock(SubContentButton.Component, subActiveColor, subActiveColor * 1.3f);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ConstructEvaluateHolder(GameObject parent);
|
||||
|
||||
|
||||
public virtual GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
// Main layout
|
||||
|
||||
UIRoot = UIFactory.CreateUIObject(this.GetType().Name, parent, new Vector2(100, 30));
|
||||
Rect = UIRoot.GetComponent<RectTransform>();
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(UIRoot, false, false, true, true, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 30, flexibleHeight: 600);
|
||||
UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
var horiRow = UIFactory.CreateUIObject("HoriGroup", UIRoot);
|
||||
UIFactory.SetLayoutElement(horiRow, minHeight: 29, flexibleHeight: 150, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(horiRow, false, false, true, true, 5, 2, childAlignment: TextAnchor.UpperLeft);
|
||||
horiRow.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// Left name label
|
||||
|
||||
NameLabel = UIFactory.CreateLabel(horiRow, "NameLabel", "<notset>", TextAnchor.MiddleLeft);
|
||||
NameLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
NameLayout = UIFactory.SetLayoutElement(NameLabel.gameObject, minHeight: 25, minWidth: 20, flexibleHeight: 300, flexibleWidth: 0);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(NameLabel.gameObject, true, true, true, true);
|
||||
|
||||
HiddenNameLabel = UIFactory.CreateInputField(NameLabel.gameObject, "HiddenNameLabel", "");
|
||||
var hiddenRect = HiddenNameLabel.Component.GetComponent<RectTransform>();
|
||||
hiddenRect.anchorMin = Vector2.zero;
|
||||
hiddenRect.anchorMax = Vector2.one;
|
||||
HiddenNameLabel.Component.readOnly = true;
|
||||
HiddenNameLabel.Component.lineType = UnityEngine.UI.InputField.LineType.MultiLineNewline;
|
||||
HiddenNameLabel.Component.textComponent.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
HiddenNameLabel.Component.gameObject.GetComponent<Image>().color = Color.clear;
|
||||
HiddenNameLabel.Component.textComponent.color = Color.clear;
|
||||
UIFactory.SetLayoutElement(HiddenNameLabel.Component.gameObject, minHeight: 25, minWidth: 20, flexibleHeight: 300, flexibleWidth: 0);
|
||||
|
||||
// Right vertical group
|
||||
|
||||
var rightGroupHolder = UIFactory.CreateUIObject("RightGroup", horiRow);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(rightGroupHolder, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(rightGroupHolder, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800);
|
||||
RightGroupLayout = rightGroupHolder.GetComponent<LayoutElement>();
|
||||
|
||||
ConstructEvaluateHolder(rightGroupHolder);
|
||||
|
||||
// Right horizontal group
|
||||
|
||||
var rightHoriGroup = UIFactory.CreateUIObject("RightHoriGroup", rightGroupHolder);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(rightHoriGroup, false, false, true, true, 4, childAlignment: TextAnchor.UpperLeft);
|
||||
UIFactory.SetLayoutElement(rightHoriGroup, minHeight: 25, minWidth: 200, flexibleWidth: 9999, flexibleHeight: 800);
|
||||
|
||||
SubContentButton = UIFactory.CreateButton(rightHoriGroup, "SubContentButton", "▲", subInactiveColor);
|
||||
UIFactory.SetLayoutElement(SubContentButton.Component.gameObject, minWidth: 25, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
SubContentButton.OnClick += SubContentClicked;
|
||||
|
||||
// Type label
|
||||
|
||||
TypeLabel = UIFactory.CreateLabel(rightHoriGroup, "ReturnLabel", "<notset>", TextAnchor.MiddleLeft);
|
||||
TypeLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UIFactory.SetLayoutElement(TypeLabel.gameObject, minHeight: 25, flexibleHeight: 150, minWidth: 60, flexibleWidth: 0);
|
||||
|
||||
// Bool and number value interaction
|
||||
|
||||
var toggleObj = UIFactory.CreateToggle(rightHoriGroup, "Toggle", out Toggle, out ToggleText);
|
||||
UIFactory.SetLayoutElement(toggleObj, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
ToggleText.color = SignatureHighlighter.KeywordBlue;
|
||||
Toggle.onValueChanged.AddListener(ToggleClicked);
|
||||
|
||||
InputField = UIFactory.CreateInputField(rightHoriGroup, "InputField", "...");
|
||||
UIFactory.SetLayoutElement(InputField.UIRoot, minWidth: 150, flexibleWidth: 0, minHeight: 25, flexibleHeight: 0);
|
||||
|
||||
// Apply
|
||||
|
||||
ApplyButton = UIFactory.CreateButton(rightHoriGroup, "ApplyButton", "Apply", new Color(0.15f, 0.19f, 0.15f));
|
||||
UIFactory.SetLayoutElement(ApplyButton.Component.gameObject, minWidth: 70, minHeight: 25, flexibleWidth: 0, flexibleHeight: 0);
|
||||
ApplyButton.OnClick += ApplyClicked;
|
||||
|
||||
// Inspect
|
||||
|
||||
InspectButton = UIFactory.CreateButton(rightHoriGroup, "InspectButton", "Inspect", new Color(0.15f, 0.15f, 0.15f));
|
||||
UIFactory.SetLayoutElement(InspectButton.Component.gameObject, minWidth: 70, flexibleWidth: 0, minHeight: 25);
|
||||
InspectButton.OnClick += InspectClicked;
|
||||
|
||||
// Main value label
|
||||
|
||||
ValueLabel = UIFactory.CreateLabel(rightHoriGroup, "ValueLabel", "Value goes here", TextAnchor.MiddleLeft);
|
||||
ValueLabel.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UIFactory.SetLayoutElement(ValueLabel.gameObject, minHeight: 25, flexibleHeight: 150, flexibleWidth: 9999);
|
||||
|
||||
// Subcontent
|
||||
|
||||
SubContentHolder = UIFactory.CreateUIObject("SubContent", UIRoot);
|
||||
UIFactory.SetLayoutElement(SubContentHolder.gameObject, minHeight: 30, flexibleHeight: 600, minWidth: 100, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(SubContentHolder, true, true, true, true, 2, childAlignment: TextAnchor.UpperLeft);
|
||||
//SubContentHolder.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.MinSize;
|
||||
SubContentHolder.SetActive(false);
|
||||
|
||||
// Bottom separator
|
||||
var separator = UIFactory.CreateUIObject("BottomSeperator", UIRoot);
|
||||
UIFactory.SetLayoutElement(separator, minHeight: 1, flexibleHeight: 0, flexibleWidth: 9999);
|
||||
separator.AddComponent<Image>().color = Color.black;
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
287
src/CacheObject/Views/EvaluateWidget.cs
Normal file
287
src/CacheObject/Views/EvaluateWidget.cs
Normal file
@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.UI;
|
||||
using UnityExplorer.UI.Models;
|
||||
using UnityExplorer.UI.Widgets.AutoComplete;
|
||||
|
||||
namespace UnityExplorer.CacheObject.Views
|
||||
{
|
||||
public class EvaluateWidget : IPooledObject
|
||||
{
|
||||
public CacheMember Owner { get; set; }
|
||||
|
||||
public GameObject UIRoot { get; set; }
|
||||
public float DefaultHeight => -1f;
|
||||
|
||||
private ParameterInfo[] arguments;
|
||||
private string[] argumentInput;
|
||||
|
||||
private GameObject argHolder;
|
||||
private readonly List<GameObject> argRows = new List<GameObject>();
|
||||
private readonly List<Text> argLabels = new List<Text>();
|
||||
|
||||
private Type[] genericArguments;
|
||||
private string[] genericInput;
|
||||
|
||||
private GameObject genericArgHolder;
|
||||
private readonly List<GameObject> genericArgRows = new List<GameObject>();
|
||||
private readonly List<Text> genericArgLabels = new List<Text>();
|
||||
private readonly List<TypeCompleter> genericAutocompleters = new List<TypeCompleter>();
|
||||
|
||||
//private readonly List<InputFieldRef> inputFields = new List<InputFieldRef>();
|
||||
private readonly List<InputFieldRef> argInputFields = new List<InputFieldRef>();
|
||||
private readonly List<InputFieldRef> genericInputFields = new List<InputFieldRef>();
|
||||
|
||||
public void OnBorrowedFromPool(CacheMember owner)
|
||||
{
|
||||
this.Owner = owner;
|
||||
|
||||
arguments = owner.Arguments;
|
||||
argumentInput = new string[arguments.Length];
|
||||
|
||||
genericArguments = owner.GenericArguments;
|
||||
genericInput = new string[genericArguments.Length];
|
||||
|
||||
SetArgRows();
|
||||
|
||||
this.UIRoot.SetActive(true);
|
||||
}
|
||||
|
||||
public void OnReturnToPool()
|
||||
{
|
||||
foreach (var input in argInputFields)
|
||||
input.Text = "";
|
||||
foreach (var input in genericInputFields)
|
||||
input.Text = "";
|
||||
|
||||
this.Owner = null;
|
||||
}
|
||||
|
||||
public Type[] TryParseGenericArguments()
|
||||
{
|
||||
Type[] outArgs = new Type[genericArguments.Length];
|
||||
|
||||
for (int i = 0; i < genericArguments.Length; i++)
|
||||
{
|
||||
outArgs[i] = ReflectionUtility.GetTypeByName(genericInput[i])
|
||||
?? throw new Exception($"Could not find any type by name '{genericInput[i]}'!");
|
||||
}
|
||||
|
||||
return outArgs;
|
||||
}
|
||||
|
||||
public object[] TryParseArguments()
|
||||
{
|
||||
object[] outArgs = new object[arguments.Length];
|
||||
|
||||
for (int i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
var arg = arguments[i];
|
||||
var input = argumentInput[i];
|
||||
|
||||
var type = arg.ParameterType;
|
||||
if (type.IsByRef)
|
||||
type = type.GetElementType();
|
||||
|
||||
if (type == typeof(string))
|
||||
{
|
||||
outArgs[i] = input;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
if (arg.IsOptional)
|
||||
outArgs[i] = arg.DefaultValue;
|
||||
else
|
||||
outArgs[i] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ParseUtility.TryParse(input, type, out outArgs[i], out Exception ex))
|
||||
{
|
||||
outArgs[i] = null;
|
||||
ExplorerCore.LogWarning($"Cannot parse argument '{arg.Name}' ({arg.ParameterType.Name})" +
|
||||
$"{(ex == null ? "" : $", {ex.GetType().Name}: {ex.Message}")}");
|
||||
}
|
||||
}
|
||||
|
||||
return outArgs;
|
||||
}
|
||||
|
||||
private void SetArgRows()
|
||||
{
|
||||
if (genericArguments.Any())
|
||||
{
|
||||
genericArgHolder.SetActive(true);
|
||||
SetGenericRows();
|
||||
}
|
||||
else
|
||||
genericArgHolder.SetActive(false);
|
||||
|
||||
if (arguments.Any())
|
||||
{
|
||||
argHolder.SetActive(true);
|
||||
SetNormalArgRows();
|
||||
}
|
||||
else
|
||||
argHolder.SetActive(false);
|
||||
}
|
||||
|
||||
private void SetGenericRows()
|
||||
{
|
||||
for (int i = 0; i < genericArguments.Length || i < genericArgRows.Count; i++)
|
||||
{
|
||||
if (i >= genericArguments.Length)
|
||||
{
|
||||
if (i >= genericArgRows.Count)
|
||||
break;
|
||||
else
|
||||
// exceeded actual args, but still iterating so there must be views left, disable them
|
||||
genericArgRows[i].SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var arg = genericArguments[i];
|
||||
|
||||
if (i >= genericArgRows.Count)
|
||||
AddArgRow(i, true);
|
||||
|
||||
genericArgRows[i].SetActive(true);
|
||||
|
||||
var autoCompleter = genericAutocompleters[i];
|
||||
autoCompleter.BaseType = arg;
|
||||
autoCompleter.CacheTypes();
|
||||
|
||||
var constraints = arg.GetGenericParameterConstraints();
|
||||
autoCompleter.GenericConstraints = constraints;
|
||||
|
||||
var sb = new StringBuilder($"<color={SignatureHighlighter.CONST}>{arg.Name}</color>");
|
||||
|
||||
for (int j = 0; j < constraints.Length; j++)
|
||||
{
|
||||
if (j == 0) sb.Append(' ').Append('(');
|
||||
else sb.Append(',').Append(' ');
|
||||
|
||||
sb.Append(SignatureHighlighter.Parse(constraints[j], false));
|
||||
|
||||
if (j + 1 == constraints.Length)
|
||||
sb.Append(')');
|
||||
}
|
||||
|
||||
genericArgLabels[i].text = sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetNormalArgRows()
|
||||
{
|
||||
for (int i = 0; i < arguments.Length || i < argRows.Count; i++)
|
||||
{
|
||||
if (i >= arguments.Length)
|
||||
{
|
||||
if (i >= argRows.Count)
|
||||
break;
|
||||
else
|
||||
// exceeded actual args, but still iterating so there must be views left, disable them
|
||||
argRows[i].SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var arg = arguments[i];
|
||||
|
||||
|
||||
if (i >= argRows.Count)
|
||||
AddArgRow(i, false);
|
||||
|
||||
argRows[i].SetActive(true);
|
||||
argLabels[i].text = $"{SignatureHighlighter.Parse(arg.ParameterType, false)} <color={SignatureHighlighter.LOCAL_ARG}>{arg.Name}</color>";
|
||||
if (arg.ParameterType == typeof(string))
|
||||
argInputFields[i].PlaceholderText.text = "";
|
||||
else
|
||||
{
|
||||
var elemType = arg.ParameterType;
|
||||
if (elemType.IsByRef)
|
||||
elemType = elemType.GetElementType();
|
||||
argInputFields[i].PlaceholderText.text = $"eg. {ParseUtility.GetExampleInput(elemType)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddArgRow(int index, bool generic)
|
||||
{
|
||||
if (!generic)
|
||||
AddArgRow(index, argHolder, argRows, argLabels, argumentInput, false);
|
||||
else
|
||||
AddArgRow(index, genericArgHolder, genericArgRows, genericArgLabels, genericInput, true);
|
||||
}
|
||||
|
||||
private void AddArgRow(int index, GameObject parent, List<GameObject> objectList, List<Text> labelList, string[] inputArray, bool generic)
|
||||
{
|
||||
var horiGroup = UIFactory.CreateUIObject("ArgRow_" + index, parent);
|
||||
UIFactory.SetLayoutElement(horiGroup, minHeight: 25, flexibleHeight: 50, minWidth: 50, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutGroup<HorizontalLayoutGroup>(horiGroup, false, false, true, true, 5);
|
||||
horiGroup.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
objectList.Add(horiGroup);
|
||||
|
||||
var label = UIFactory.CreateLabel(horiGroup, "ArgLabel", "not set", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(label.gameObject, minWidth: 40, flexibleWidth: 90, minHeight: 25, flexibleHeight: 50);
|
||||
labelList.Add(label);
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
|
||||
var inputField = UIFactory.CreateInputField(horiGroup, "InputField", "...");
|
||||
UIFactory.SetLayoutElement(inputField.UIRoot, minHeight: 25, flexibleHeight: 50, minWidth: 100, flexibleWidth: 1000);
|
||||
inputField.Component.lineType = InputField.LineType.MultiLineNewline;
|
||||
inputField.UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
inputField.OnValueChanged += (string val) => { inputArray[index] = val; };
|
||||
|
||||
if (!generic)
|
||||
argInputFields.Add(inputField);
|
||||
else
|
||||
genericInputFields.Add(inputField);
|
||||
|
||||
if (generic)
|
||||
genericAutocompleters.Add(new TypeCompleter(null, inputField));
|
||||
}
|
||||
|
||||
public GameObject CreateContent(GameObject parent)
|
||||
{
|
||||
UIRoot = UIFactory.CreateVerticalGroup(parent, "EvaluateWidget", false, false, true, true, 3, new Vector4(2, 2, 2, 2),
|
||||
new Color(0.15f, 0.15f, 0.15f));
|
||||
UIFactory.SetLayoutElement(UIRoot, minWidth: 50, flexibleWidth: 9999, minHeight: 50, flexibleHeight: 800);
|
||||
//UIRoot.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// generic args
|
||||
this.genericArgHolder = UIFactory.CreateUIObject("GenericHolder", UIRoot);
|
||||
UIFactory.SetLayoutElement(genericArgHolder, flexibleWidth: 1000);
|
||||
var genericsTitle = UIFactory.CreateLabel(genericArgHolder, "GenericsTitle", "Generic Arguments", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(genericsTitle.gameObject, minHeight: 25, flexibleWidth: 1000);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(genericArgHolder, false, false, true, true, 3);
|
||||
UIFactory.SetLayoutElement(genericArgHolder, minHeight: 25, flexibleHeight: 750, minWidth: 50, flexibleWidth: 9999);
|
||||
//genericArgHolder.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// args
|
||||
this.argHolder = UIFactory.CreateUIObject("ArgHolder", UIRoot);
|
||||
UIFactory.SetLayoutElement(argHolder, flexibleWidth: 1000);
|
||||
var argsTitle = UIFactory.CreateLabel(argHolder, "ArgsTitle", "Arguments", TextAnchor.MiddleLeft);
|
||||
UIFactory.SetLayoutElement(argsTitle.gameObject, minHeight: 25, flexibleWidth: 1000);
|
||||
UIFactory.SetLayoutGroup<VerticalLayoutGroup>(argHolder, false, false, true, true, 3);
|
||||
UIFactory.SetLayoutElement(argHolder, minHeight: 25, flexibleHeight: 750, minWidth: 50, flexibleWidth: 9999);
|
||||
//argHolder.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// evaluate button
|
||||
var evalButton = UIFactory.CreateButton(UIRoot, "EvaluateButton", "Evaluate", new Color(0.2f, 0.2f, 0.2f));
|
||||
UIFactory.SetLayoutElement(evalButton.Component.gameObject, minHeight: 25, minWidth: 150, flexibleWidth: 0);
|
||||
evalButton.OnClick += () =>
|
||||
{
|
||||
Owner.EvaluateAndSetCell();
|
||||
};
|
||||
|
||||
return UIRoot;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
#if MONO
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer.Core.CSharp
|
||||
{
|
||||
public class DummyBehaviour : MonoBehaviour
|
||||
{
|
||||
public static DummyBehaviour Instance;
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
var obj = new GameObject("Explorer_DummyBehaviour");
|
||||
DontDestroyOnLoad(obj);
|
||||
obj.hideFlags |= HideFlags.HideAndDontSave;
|
||||
|
||||
obj.AddComponent<DummyBehaviour>();
|
||||
}
|
||||
|
||||
internal void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using Mono.CSharp;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using UnityExplorer.UI.Main.CSConsole;
|
||||
using UnityExplorer.UI.Main.Home;
|
||||
using UnityExplorer.UI.Inspectors;
|
||||
|
||||
namespace UnityExplorer.Core.CSharp
|
||||
{
|
||||
public class ScriptInteraction : InteractiveBase
|
||||
{
|
||||
public static void Log(object message)
|
||||
{
|
||||
ExplorerCore.Log(message);
|
||||
}
|
||||
|
||||
public static void StartCoroutine(IEnumerator ienumerator)
|
||||
{
|
||||
RuntimeProvider.Instance.StartConsoleCoroutine(ienumerator);
|
||||
}
|
||||
|
||||
public static void AddUsing(string directive)
|
||||
{
|
||||
CSharpConsole.Instance.AddUsing(directive);
|
||||
}
|
||||
|
||||
public static void GetUsing()
|
||||
{
|
||||
ExplorerCore.Log(CSharpConsole.Instance.Evaluator.GetUsing());
|
||||
}
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
CSharpConsole.Instance.ResetConsole();
|
||||
}
|
||||
|
||||
public static object CurrentTarget()
|
||||
{
|
||||
return InspectorManager.Instance?.m_activeInspector?.Target;
|
||||
}
|
||||
|
||||
public static object[] AllTargets()
|
||||
{
|
||||
int count = InspectorManager.Instance?.m_currentInspectors.Count ?? 0;
|
||||
object[] ret = new object[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ret[i] = InspectorManager.Instance?.m_currentInspectors[i].Target;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static void Inspect(object obj)
|
||||
{
|
||||
InspectorManager.Instance.Inspect(obj);
|
||||
}
|
||||
|
||||
public static void Inspect(Type type)
|
||||
{
|
||||
InspectorManager.Instance.Inspect(type);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Core;
|
||||
using UnityExplorer.UI.Main.CSConsole;
|
||||
|
||||
namespace UnityExplorer.Core.CSharp
|
||||
{
|
||||
public struct Suggestion
|
||||
{
|
||||
public enum Contexts
|
||||
{
|
||||
Namespace,
|
||||
Keyword,
|
||||
Other
|
||||
}
|
||||
|
||||
// ~~~~ Instance ~~~~
|
||||
|
||||
public readonly string Prefix;
|
||||
public readonly string Addition;
|
||||
public readonly Contexts Context;
|
||||
|
||||
public string Full => Prefix + Addition;
|
||||
|
||||
public Color TextColor => GetTextColor();
|
||||
|
||||
public Suggestion(string addition, string prefix, Contexts type)
|
||||
{
|
||||
Addition = addition;
|
||||
Prefix = prefix;
|
||||
Context = type;
|
||||
}
|
||||
|
||||
private Color GetTextColor()
|
||||
{
|
||||
switch (Context)
|
||||
{
|
||||
case Contexts.Namespace: return Color.grey;
|
||||
case Contexts.Keyword: return keywordColor;
|
||||
default: return Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~ Static ~~~~
|
||||
|
||||
public static HashSet<string> Namespaces => m_namespaces ?? GetNamespaces();
|
||||
private static HashSet<string> m_namespaces;
|
||||
|
||||
public static HashSet<string> Keywords => m_keywords ?? (m_keywords = new HashSet<string>(CSLexerHighlighter.validKeywordMatcher.Keywords));
|
||||
private static HashSet<string> m_keywords;
|
||||
|
||||
private static readonly Color keywordColor = new Color(80f / 255f, 150f / 255f, 215f / 255f);
|
||||
|
||||
private static HashSet<string> GetNamespaces()
|
||||
{
|
||||
HashSet<string> set = new HashSet<string>(
|
||||
AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(GetTypes)
|
||||
.Where(x => x.IsPublic && !string.IsNullOrEmpty(x.Namespace))
|
||||
.Select(x => x.Namespace));
|
||||
|
||||
return m_namespaces = set;
|
||||
|
||||
IEnumerable<Type> GetTypes(Assembly asm) => asm.TryGetTypes();
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,10 @@ namespace UnityExplorer.Core.Config
|
||||
|
||||
public object DefaultValue { get; }
|
||||
|
||||
public ConfigHandler Handler => IsInternal
|
||||
? ConfigManager.InternalHandler
|
||||
: ConfigManager.Handler;
|
||||
|
||||
public T Value
|
||||
{
|
||||
get => m_value;
|
||||
@ -51,19 +55,19 @@ namespace UnityExplorer.Core.Config
|
||||
|
||||
m_value = value;
|
||||
|
||||
ConfigManager.Handler.SetConfigValue(this, value);
|
||||
Handler.SetConfigValue(this, value);
|
||||
|
||||
OnValueChanged?.Invoke(value);
|
||||
OnValueChangedNotify?.Invoke();
|
||||
|
||||
ConfigManager.Handler.OnAnyConfigChanged();
|
||||
Handler.OnAnyConfigChanged();
|
||||
}
|
||||
|
||||
object IConfigElement.GetLoaderConfigValue() => GetLoaderConfigValue();
|
||||
|
||||
public T GetLoaderConfigValue()
|
||||
{
|
||||
return ConfigManager.Handler.GetConfigValue(this);
|
||||
return Handler.GetConfigValue(this);
|
||||
}
|
||||
|
||||
public void RevertToDefaultValue()
|
||||
|
@ -6,8 +6,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.UI.Main;
|
||||
using UnityExplorer.UI.Main.Home;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.Core.Config
|
||||
{
|
||||
@ -17,230 +16,118 @@ namespace UnityExplorer.Core.Config
|
||||
// See the UnityExplorer.Loader namespace for the implementations.
|
||||
public static ConfigHandler Handler { get; private set; }
|
||||
|
||||
public static ConfigElement<KeyCode> Main_Menu_Toggle;
|
||||
public static ConfigElement<bool> Force_Unlock_Mouse;
|
||||
public static ConfigElement<int> Default_Page_Limit;
|
||||
public static ConfigElement<string> Default_Output_Path;
|
||||
public static ConfigElement<bool> Log_Unity_Debug;
|
||||
public static ConfigElement<bool> Hide_On_Startup;
|
||||
public static ConfigElement<KeyCode> Master_Toggle;
|
||||
public static ConfigElement<UIManager.VerticalAnchor> Main_Navbar_Anchor;
|
||||
public static ConfigElement<bool> Force_Unlock_Mouse;
|
||||
public static ConfigElement<KeyCode> Force_Unlock_Toggle;
|
||||
public static ConfigElement<bool> Aggressive_Mouse_Unlock;
|
||||
public static ConfigElement<bool> Disable_EventSystem_Override;
|
||||
public static ConfigElement<string> Default_Output_Path;
|
||||
public static ConfigElement<bool> Log_Unity_Debug;
|
||||
public static ConfigElement<bool> Hide_On_Startup;
|
||||
public static ConfigElement<float> Startup_Delay_Time;
|
||||
|
||||
public static ConfigElement<string> Last_Window_Anchors;
|
||||
public static ConfigElement<string> Last_Window_Position;
|
||||
public static ConfigElement<int> Last_Active_Tab;
|
||||
public static ConfigElement<bool> Last_DebugConsole_State;
|
||||
public static ConfigElement<bool> Last_SceneExplorer_State;
|
||||
public static ConfigElement<string> Reflection_Signature_Blacklist;
|
||||
|
||||
// internal configs
|
||||
internal static InternalConfigHandler InternalHandler { get; private set; }
|
||||
|
||||
public static ConfigElement<string> ObjectExplorerData;
|
||||
public static ConfigElement<string> InspectorData;
|
||||
public static ConfigElement<string> CSConsoleData;
|
||||
public static ConfigElement<string> OptionsPanelData;
|
||||
public static ConfigElement<string> ConsoleLogData;
|
||||
public static ConfigElement<string> HookManagerData;
|
||||
|
||||
internal static readonly Dictionary<string, IConfigElement> ConfigElements = new Dictionary<string, IConfigElement>();
|
||||
internal static readonly Dictionary<string, IConfigElement> InternalConfigs = new Dictionary<string, IConfigElement>();
|
||||
|
||||
public static void Init(ConfigHandler configHandler)
|
||||
{
|
||||
Handler = configHandler;
|
||||
Handler.Init();
|
||||
|
||||
InternalHandler = new InternalConfigHandler();
|
||||
InternalHandler.Init();
|
||||
|
||||
CreateConfigElements();
|
||||
|
||||
Handler.LoadConfig();
|
||||
InternalHandler.LoadConfig();
|
||||
|
||||
SceneExplorer.OnToggleShow += SceneExplorer_OnToggleShow;
|
||||
PanelDragger.OnFinishResize += PanelDragger_OnFinishResize;
|
||||
PanelDragger.OnFinishDrag += PanelDragger_OnFinishDrag;
|
||||
MainMenu.OnActiveTabChanged += MainMenu_OnActiveTabChanged;
|
||||
DebugConsole.OnToggleShow += DebugConsole_OnToggleShow;
|
||||
|
||||
InitConsoleCallback();
|
||||
//InitConsoleCallback();
|
||||
}
|
||||
|
||||
internal static void RegisterConfigElement<T>(ConfigElement<T> configElement)
|
||||
{
|
||||
Handler.RegisterConfigElement(configElement);
|
||||
ConfigElements.Add(configElement.Name, configElement);
|
||||
if (!configElement.IsInternal)
|
||||
{
|
||||
Handler.RegisterConfigElement(configElement);
|
||||
ConfigElements.Add(configElement.Name, configElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalHandler.RegisterConfigElement(configElement);
|
||||
InternalConfigs.Add(configElement.Name, configElement);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateConfigElements()
|
||||
{
|
||||
Main_Menu_Toggle = new ConfigElement<KeyCode>("Main Menu Toggle",
|
||||
"The UnityEngine.KeyCode to toggle the UnityExplorer Menu.",
|
||||
Master_Toggle = new ConfigElement<KeyCode>("UnityExplorer Toggle",
|
||||
"The key to enable or disable UnityExplorer's menu and features.",
|
||||
KeyCode.F7);
|
||||
|
||||
Main_Navbar_Anchor = new ConfigElement<UIManager.VerticalAnchor>("Main Navbar Anchor",
|
||||
"The vertical anchor of the main UnityExplorer Navbar, in case you want to move it.",
|
||||
UIManager.VerticalAnchor.Top);
|
||||
|
||||
Hide_On_Startup = new ConfigElement<bool>("Hide On Startup",
|
||||
"Should UnityExplorer be hidden on startup?",
|
||||
false);
|
||||
|
||||
Log_Unity_Debug = new ConfigElement<bool>("Log Unity Debug",
|
||||
"Should UnityEngine.Debug.Log messages be printed to UnityExplorer's log?",
|
||||
false);
|
||||
|
||||
Force_Unlock_Mouse = new ConfigElement<bool>("Force Unlock Mouse",
|
||||
"Force the Cursor to be unlocked (visible) when the UnityExplorer menu is open.",
|
||||
true);
|
||||
|
||||
Default_Page_Limit = new ConfigElement<int>("Default Page Limit",
|
||||
"The default maximum number of elements per 'page' in UnityExplorer.",
|
||||
25);
|
||||
Force_Unlock_Toggle = new ConfigElement<KeyCode>("Force Unlock Toggle Key",
|
||||
"The keybind to toggle the 'Force Unlock Mouse' setting. Only usable when UnityExplorer is open.",
|
||||
KeyCode.None);
|
||||
|
||||
Aggressive_Mouse_Unlock = new ConfigElement<bool>("Aggressive Mouse Unlock",
|
||||
"Use WaitForEndOfFrame to aggressively force the Mouse to be unlocked.\n<b>Requires restart to take effect.</b>",
|
||||
false);
|
||||
|
||||
Disable_EventSystem_Override = new ConfigElement<bool>("Disable EventSystem override",
|
||||
"If enabled, UnityExplorer will not override the EventSystem from the game.\n<b>May require restart to take effect.</b>",
|
||||
false);
|
||||
|
||||
Log_Unity_Debug = new ConfigElement<bool>("Log Unity Debug",
|
||||
"Should UnityEngine.Debug.Log messages be printed to UnityExplorer's log?",
|
||||
false);
|
||||
|
||||
Default_Output_Path = new ConfigElement<string>("Default Output Path",
|
||||
"The default output path when exporting things from UnityExplorer.",
|
||||
Path.Combine(ExplorerCore.Loader.ExplorerFolder, "Output"));
|
||||
|
||||
// Internal configs
|
||||
Startup_Delay_Time = new ConfigElement<float>("Startup Delay Time",
|
||||
"The delay on startup before the UI is created.",
|
||||
1f);
|
||||
|
||||
Last_Window_Anchors = new ConfigElement<string>("Last_Window_Anchors",
|
||||
"For internal use, the last anchors of the UnityExplorer window.",
|
||||
DEFAULT_WINDOW_ANCHORS,
|
||||
true);
|
||||
Reflection_Signature_Blacklist = new ConfigElement<string>("Member Signature Blacklist",
|
||||
"Use this to blacklist certain member signatures if they are known to cause a crash or other issues.\r\n" +
|
||||
"Seperate signatures with a semicolon ';'.\r\n" +
|
||||
"For example, to blacklist Camera.main, you would add 'UnityEngine.Camera.main;'",
|
||||
"");
|
||||
|
||||
Last_Window_Position = new ConfigElement<string>("Last_Window_Position",
|
||||
"For internal use, the last position of the UnityExplorer window.",
|
||||
DEFAULT_WINDOW_POSITION,
|
||||
true);
|
||||
// Internal configs (panel save data)
|
||||
|
||||
Last_Active_Tab = new ConfigElement<int>("Last_Active_Tab",
|
||||
"For internal use, the last active tab index.",
|
||||
0,
|
||||
true);
|
||||
|
||||
Last_DebugConsole_State = new ConfigElement<bool>("Last_DebugConsole_State",
|
||||
"For internal use, the collapsed state of the Debug Console.",
|
||||
true,
|
||||
true);
|
||||
|
||||
Last_SceneExplorer_State = new ConfigElement<bool>("Last_SceneExplorer_State",
|
||||
"For internal use, the collapsed state of the Scene Explorer.",
|
||||
true,
|
||||
true);
|
||||
ObjectExplorerData = new ConfigElement<string>("ObjectExplorer", "", "", true);
|
||||
InspectorData = new ConfigElement<string>("Inspector", "", "", true);
|
||||
CSConsoleData = new ConfigElement<string>("CSConsole", "", "", true);
|
||||
OptionsPanelData = new ConfigElement<string>("OptionsPanel", "", "", true);
|
||||
ConsoleLogData = new ConfigElement<string>("ConsoleLog", "", "", true);
|
||||
HookManagerData = new ConfigElement<string>("HookManager", "", "", true);
|
||||
}
|
||||
|
||||
// Internal config callback listeners
|
||||
|
||||
private static void PanelDragger_OnFinishResize(RectTransform rect)
|
||||
{
|
||||
Last_Window_Anchors.Value = rect.RectAnchorsToString();
|
||||
}
|
||||
|
||||
private static void PanelDragger_OnFinishDrag(RectTransform rect)
|
||||
{
|
||||
Last_Window_Position.Value = rect.RectPositionToString();
|
||||
}
|
||||
|
||||
private static void MainMenu_OnActiveTabChanged(int page)
|
||||
{
|
||||
Last_Active_Tab.Value = page;
|
||||
}
|
||||
|
||||
private static void DebugConsole_OnToggleShow(bool showing)
|
||||
{
|
||||
Last_DebugConsole_State.Value = showing;
|
||||
}
|
||||
|
||||
private static void SceneExplorer_OnToggleShow(bool showing)
|
||||
{
|
||||
Last_SceneExplorer_State.Value = showing;
|
||||
}
|
||||
|
||||
#region CONSOLE ONEXIT CALLBACK
|
||||
|
||||
internal static void InitConsoleCallback()
|
||||
{
|
||||
handler = new ConsoleEventDelegate(ConsoleEventCallback);
|
||||
SetConsoleCtrlHandler(handler, true);
|
||||
}
|
||||
|
||||
static bool ConsoleEventCallback(int eventType)
|
||||
{
|
||||
// 2 is Console Quit
|
||||
if (eventType == 2)
|
||||
Handler.SaveConfig();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static ConsoleEventDelegate handler;
|
||||
private delegate bool ConsoleEventDelegate(int eventType);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add);
|
||||
|
||||
#endregion
|
||||
|
||||
#region WINDOW ANCHORS / POSITION HELPERS
|
||||
|
||||
// Window Anchors helpers
|
||||
|
||||
private const string DEFAULT_WINDOW_ANCHORS = "0.25,0.10,0.78,0.95";
|
||||
private const string DEFAULT_WINDOW_POSITION = "0,0";
|
||||
|
||||
internal static CultureInfo _enCulture = new CultureInfo("en-US");
|
||||
|
||||
internal static string RectAnchorsToString(this RectTransform rect)
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.Format(_enCulture, "{0},{1},{2},{3}", new object[]
|
||||
{
|
||||
rect.anchorMin.x,
|
||||
rect.anchorMin.y,
|
||||
rect.anchorMax.x,
|
||||
rect.anchorMax.y
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DEFAULT_WINDOW_ANCHORS;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetAnchorsFromString(this RectTransform panel, string stringAnchors)
|
||||
{
|
||||
Vector4 anchors;
|
||||
try
|
||||
{
|
||||
var split = stringAnchors.Split(',');
|
||||
|
||||
if (split.Length != 4)
|
||||
throw new Exception();
|
||||
|
||||
anchors.x = float.Parse(split[0], _enCulture);
|
||||
anchors.y = float.Parse(split[1], _enCulture);
|
||||
anchors.z = float.Parse(split[2], _enCulture);
|
||||
anchors.w = float.Parse(split[3], _enCulture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
anchors = new Vector4(0.25f, 0.1f, 0.78f, 0.95f);
|
||||
}
|
||||
|
||||
panel.anchorMin = new Vector2(anchors.x, anchors.y);
|
||||
panel.anchorMax = new Vector2(anchors.z, anchors.w);
|
||||
}
|
||||
|
||||
internal static string RectPositionToString(this RectTransform rect)
|
||||
{
|
||||
return string.Format(_enCulture, "{0},{1}", new object[]
|
||||
{
|
||||
rect.localPosition.x, rect.localPosition.y
|
||||
});
|
||||
}
|
||||
|
||||
internal static void SetPositionFromString(this RectTransform rect, string stringPosition)
|
||||
{
|
||||
try
|
||||
{
|
||||
var split = stringPosition.Split(',');
|
||||
|
||||
if (split.Length != 2)
|
||||
throw new Exception();
|
||||
|
||||
Vector3 vector = rect.localPosition;
|
||||
vector.x = float.Parse(split[0], _enCulture);
|
||||
vector.y = float.Parse(split[1], _enCulture);
|
||||
rect.localPosition = vector;
|
||||
}
|
||||
catch //(Exception ex)
|
||||
{
|
||||
//ExplorerCore.LogWarning("Exception setting window position: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
105
src/Core/Config/InternalConfigHandler.cs
Normal file
105
src/Core/Config/InternalConfigHandler.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using IniParser.Parser;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.UI;
|
||||
|
||||
namespace UnityExplorer.Core.Config
|
||||
{
|
||||
public class InternalConfigHandler : ConfigHandler
|
||||
{
|
||||
internal static IniDataParser _parser;
|
||||
internal static string INI_PATH;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
INI_PATH = Path.Combine(ExplorerCore.Loader.ExplorerFolder, "data.ini");
|
||||
_parser = new IniDataParser();
|
||||
_parser.Configuration.CommentString = "#";
|
||||
}
|
||||
|
||||
public override void LoadConfig()
|
||||
{
|
||||
if (!TryLoadConfig())
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
public override void RegisterConfigElement<T>(ConfigElement<T> element)
|
||||
{
|
||||
// Not necessary
|
||||
}
|
||||
|
||||
public override void SetConfigValue<T>(ConfigElement<T> element, T value)
|
||||
{
|
||||
// Not necessary
|
||||
}
|
||||
|
||||
public override T GetConfigValue<T>(ConfigElement<T> element)
|
||||
{
|
||||
// Not necessary, just return the value.
|
||||
return element.Value;
|
||||
}
|
||||
|
||||
public override void OnAnyConfigChanged()
|
||||
{
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
public bool TryLoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(INI_PATH))
|
||||
return false;
|
||||
|
||||
string ini = File.ReadAllText(INI_PATH);
|
||||
|
||||
var data = _parser.Parse(ini);
|
||||
|
||||
foreach (var config in data.Sections["Config"])
|
||||
{
|
||||
if (ConfigManager.InternalConfigs.TryGetValue(config.KeyName, out IConfigElement configElement))
|
||||
configElement.BoxedValue = StringToConfigValue(config.Value, configElement.ElementType);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Error loading internal data: " + ex.ToString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SaveConfig()
|
||||
{
|
||||
if (UIManager.Initializing)
|
||||
return;
|
||||
|
||||
var data = new IniParser.Model.IniData();
|
||||
|
||||
data.Sections.AddSection("Config");
|
||||
var sec = data.Sections["Config"];
|
||||
|
||||
foreach (var entry in ConfigManager.InternalConfigs)
|
||||
sec.AddKey(entry.Key, entry.Value.BoxedValue.ToString());
|
||||
|
||||
File.WriteAllText(INI_PATH, data.ToString());
|
||||
}
|
||||
|
||||
public object StringToConfigValue(string value, Type elementType)
|
||||
{
|
||||
if (elementType.IsEnum)
|
||||
return Enum.Parse(elementType, value);
|
||||
else if (elementType == typeof(bool))
|
||||
return bool.Parse(value);
|
||||
else if (elementType == typeof(int))
|
||||
return int.Parse(value);
|
||||
else
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
84
src/Core/ExplorerBehaviour.cs
Normal file
84
src/Core/ExplorerBehaviour.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
#if CPP
|
||||
using UnhollowerRuntimeLib;
|
||||
#endif
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
// Handles all Behaviour update calls for UnityExplorer (Update, FixedUpdate, OnPostRender).
|
||||
// Basically just a wrapper which calls the corresponding methods in ExplorerCore.
|
||||
|
||||
public class ExplorerBehaviour : MonoBehaviour
|
||||
{
|
||||
internal static ExplorerBehaviour Instance { get; private set; }
|
||||
|
||||
internal static void Setup()
|
||||
{
|
||||
#if CPP
|
||||
ClassInjector.RegisterTypeInIl2Cpp<ExplorerBehaviour>();
|
||||
#endif
|
||||
|
||||
var obj = new GameObject("ExplorerBehaviour");
|
||||
GameObject.DontDestroyOnLoad(obj);
|
||||
obj.hideFlags |= HideFlags.HideAndDontSave;
|
||||
Instance = obj.AddComponent<ExplorerBehaviour>();
|
||||
}
|
||||
|
||||
#if CPP
|
||||
public ExplorerBehaviour(IntPtr ptr) : base(ptr) { }
|
||||
#endif
|
||||
|
||||
private static bool onPostRenderFailed;
|
||||
|
||||
internal void Awake()
|
||||
{
|
||||
try
|
||||
{
|
||||
#if CPP
|
||||
Camera.onPostRender = Camera.onPostRender == null
|
||||
? new Action<Camera>(OnPostRender)
|
||||
: Il2CppSystem.Delegate.Combine(Camera.onPostRender,
|
||||
(Camera.CameraCallback)new Action<Camera>(OnPostRender)).Cast<Camera.CameraCallback>();
|
||||
|
||||
if (Camera.onPostRender == null || Camera.onPostRender.delegates == null)
|
||||
{
|
||||
ExplorerCore.LogWarning("Failed to add Camera.onPostRender listener, falling back to LateUpdate instead!");
|
||||
onPostRenderFailed = true;
|
||||
}
|
||||
#else
|
||||
Camera.onPostRender += OnPostRender;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception adding onPostRender listener: {ex.ReflectionExToString()}\r\nFalling back to LateUpdate!");
|
||||
onPostRenderFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
ExplorerCore.Update();
|
||||
}
|
||||
|
||||
internal void FixedUpdate()
|
||||
{
|
||||
ExplorerCore.FixedUpdate();
|
||||
}
|
||||
|
||||
internal void LateUpdate()
|
||||
{
|
||||
if (onPostRenderFailed)
|
||||
OnPostRender(null);
|
||||
}
|
||||
|
||||
internal static void OnPostRender(Camera _)
|
||||
{
|
||||
ExplorerCore.OnPostRender();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,12 @@
|
||||
using System;
|
||||
using HarmonyLib;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Core.Unity;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityExplorer.Core.Input;
|
||||
using BF = System.Reflection.BindingFlags;
|
||||
using UnityExplorer.Core.Config;
|
||||
using UnityExplorer.Core;
|
||||
using UnityExplorer.Core.Config;
|
||||
using UnityExplorer.Core.Input;
|
||||
using UnityExplorer.UI;
|
||||
#if ML
|
||||
using Harmony;
|
||||
#else
|
||||
using HarmonyLib;
|
||||
#endif
|
||||
|
||||
namespace UnityExplorer.Core.Input
|
||||
{
|
||||
@ -30,50 +25,79 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public static bool ShouldActuallyUnlock => UIManager.ShowMenu && Unlock;
|
||||
|
||||
private static CursorLockMode m_lastLockMode;
|
||||
private static bool m_lastVisibleState;
|
||||
private static CursorLockMode lastLockMode;
|
||||
private static bool lastVisibleState;
|
||||
|
||||
private static bool m_currentlySettingCursor = false;
|
||||
|
||||
private static Type CursorType
|
||||
=> m_cursorType
|
||||
?? (m_cursorType = ReflectionUtility.GetTypeByName("UnityEngine.Cursor"));
|
||||
private static Type m_cursorType;
|
||||
private static bool currentlySettingCursor = false;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
SetupPatches();
|
||||
lastLockMode = Cursor.lockState;
|
||||
lastVisibleState = Cursor.visible;
|
||||
|
||||
SetupPatches();
|
||||
UpdateCursorControl();
|
||||
|
||||
// Hook up config values
|
||||
|
||||
// Force Unlock Mouse
|
||||
Unlock = ConfigManager.Force_Unlock_Mouse.Value;
|
||||
ConfigManager.Force_Unlock_Mouse.OnValueChanged += (bool val) => { Unlock = val; };
|
||||
|
||||
// Aggressive Mouse Unlock
|
||||
if (ConfigManager.Aggressive_Mouse_Unlock.Value)
|
||||
SetupAggressiveUnlock();
|
||||
}
|
||||
|
||||
public static void SetupAggressiveUnlock()
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeProvider.Instance.StartCoroutine(AggressiveUnlockCoroutine());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting up Aggressive Mouse Unlock: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static WaitForEndOfFrame _waitForEndOfFrame = new WaitForEndOfFrame();
|
||||
|
||||
private static IEnumerator AggressiveUnlockCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return _waitForEndOfFrame ?? (_waitForEndOfFrame = new WaitForEndOfFrame());
|
||||
|
||||
if (UIManager.ShowMenu)
|
||||
UpdateCursorControl();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateCursorControl()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_currentlySettingCursor = true;
|
||||
currentlySettingCursor = true;
|
||||
|
||||
if (ShouldActuallyUnlock)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
if (UIManager.EventSys)
|
||||
if (!ConfigManager.Disable_EventSystem_Override.Value && UIManager.EventSys)
|
||||
SetEventSystem();
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor.lockState = m_lastLockMode;
|
||||
Cursor.visible = m_lastVisibleState;
|
||||
Cursor.lockState = lastLockMode;
|
||||
Cursor.visible = lastVisibleState;
|
||||
|
||||
if (UIManager.EventSys)
|
||||
if (!ConfigManager.Disable_EventSystem_Override.Value && UIManager.EventSys)
|
||||
ReleaseEventSystem();
|
||||
}
|
||||
|
||||
m_currentlySettingCursor = false;
|
||||
currentlySettingCursor = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -83,31 +107,27 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
// Event system overrides
|
||||
|
||||
private static bool m_settingEventSystem;
|
||||
private static EventSystem m_lastEventSystem;
|
||||
private static BaseInputModule m_lastInputModule;
|
||||
private static bool settingEventSystem;
|
||||
private static EventSystem lastEventSystem;
|
||||
private static BaseInputModule lastInputModule;
|
||||
|
||||
public static void SetEventSystem()
|
||||
{
|
||||
// temp disabled for new InputSystem
|
||||
if (InputManager.CurrentType == InputType.InputSystem)
|
||||
return;
|
||||
|
||||
// Disable current event system object
|
||||
if (m_lastEventSystem || EventSystem.current)
|
||||
if (EventSystem.current && EventSystem.current != UIManager.EventSys)
|
||||
{
|
||||
if (!m_lastEventSystem)
|
||||
m_lastEventSystem = EventSystem.current;
|
||||
|
||||
m_lastEventSystem.enabled = false;
|
||||
lastEventSystem = EventSystem.current;
|
||||
lastEventSystem.enabled = false;
|
||||
}
|
||||
|
||||
// Set to our current system
|
||||
m_settingEventSystem = true;
|
||||
EventSystem.current = UIManager.EventSys;
|
||||
settingEventSystem = true;
|
||||
UIManager.EventSys.enabled = true;
|
||||
EventSystem.current = UIManager.EventSys;
|
||||
InputManager.ActivateUIModule();
|
||||
m_settingEventSystem = false;
|
||||
settingEventSystem = false;
|
||||
}
|
||||
|
||||
public static void ReleaseEventSystem()
|
||||
@ -115,50 +135,125 @@ namespace UnityExplorer.Core.Input
|
||||
if (InputManager.CurrentType == InputType.InputSystem)
|
||||
return;
|
||||
|
||||
if (m_lastEventSystem)
|
||||
if (lastEventSystem && lastEventSystem.gameObject.activeSelf)
|
||||
{
|
||||
m_lastEventSystem.enabled = true;
|
||||
lastEventSystem.enabled = true;
|
||||
|
||||
m_settingEventSystem = true;
|
||||
EventSystem.current = m_lastEventSystem;
|
||||
m_lastInputModule?.ActivateModule();
|
||||
m_settingEventSystem = false;
|
||||
settingEventSystem = true;
|
||||
EventSystem.current = lastEventSystem;
|
||||
lastInputModule?.ActivateModule();
|
||||
settingEventSystem = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Patches
|
||||
|
||||
private static void SetupPatches()
|
||||
public static void SetupPatches()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (CursorType == null)
|
||||
throw new Exception("Could not load Type 'UnityEngine.Cursor'!");
|
||||
PrefixPropertySetter(typeof(Cursor),
|
||||
"lockState",
|
||||
new HarmonyMethod(typeof(CursorUnlocker).GetMethod(nameof(CursorUnlocker.Prefix_set_lockState))));
|
||||
|
||||
// Get current cursor state and enable cursor
|
||||
m_lastLockMode = (CursorLockMode?)CursorType.GetProperty("lockState", BF.Public | BF.Static)?.GetValue(null, null)
|
||||
?? CursorLockMode.None;
|
||||
PrefixPropertySetter(typeof(Cursor),
|
||||
"visible",
|
||||
new HarmonyMethod(typeof(CursorUnlocker).GetMethod(nameof(CursorUnlocker.Prefix_set_visible))));
|
||||
|
||||
m_lastVisibleState = (bool?)CursorType.GetProperty("visible", BF.Public | BF.Static)?.GetValue(null, null)
|
||||
?? false;
|
||||
PrefixPropertySetter(typeof(EventSystem),
|
||||
"current",
|
||||
new HarmonyMethod(typeof(CursorUnlocker).GetMethod(nameof(CursorUnlocker.Prefix_EventSystem_set_current))));
|
||||
|
||||
ExplorerCore.Loader.SetupPatches();
|
||||
PrefixMethod(typeof(EventSystem),
|
||||
"SetSelectedGameObject",
|
||||
// some games use a modified version of uGUI that includes this extra int argument on this method.
|
||||
new Type[] { typeof(GameObject), typeof(BaseEventData), typeof(int) },
|
||||
new HarmonyMethod(typeof(CursorUnlocker).GetMethod(nameof(CursorUnlocker.Prefix_EventSystem_SetSelectedGameObject))),
|
||||
// most games use these arguments, we'll use them as our "backup".
|
||||
new Type[] { typeof(GameObject), typeof(BaseEventData) });
|
||||
|
||||
//// Not sure if this one is needed.
|
||||
//PrefixMethod(typeof(PointerInputModule),
|
||||
// "ClearSelection",
|
||||
// new Type[0],
|
||||
// new HarmonyMethod(typeof(CursorUnlocker).GetMethod(nameof(CursorUnlocker.Prefix_PointerInputModule_ClearSelection))));
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log($"Error on CursorUnlocker.Init! {e.GetType()}, {e.Message}");
|
||||
ExplorerCore.Log($"Exception setting up Harmony patches:\r\n{ex.ReflectionExToString()}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrefixMethod(Type type, string method, Type[] arguments, HarmonyMethod prefix, Type[] backupArgs = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var methodInfo = type.GetMethod(method, ReflectionUtility.FLAGS, null, arguments, null);
|
||||
if (methodInfo == null)
|
||||
{
|
||||
if (backupArgs != null)
|
||||
methodInfo = type.GetMethod(method, ReflectionUtility.FLAGS, null, backupArgs, null);
|
||||
|
||||
if (methodInfo == null)
|
||||
throw new MissingMethodException($"Could not find method for patching - '{type.FullName}.{method}'!");
|
||||
}
|
||||
|
||||
var processor = ExplorerCore.Harmony.CreateProcessor(methodInfo);
|
||||
processor.AddPrefix(prefix);
|
||||
processor.Patch();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Unable to patch {type.Name}.{method}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrefixPropertySetter(Type type, string property, HarmonyMethod prefix)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processor = ExplorerCore.Harmony.CreateProcessor(type.GetProperty(property, ReflectionUtility.FLAGS).GetSetMethod());
|
||||
processor.AddPrefix(prefix);
|
||||
processor.Patch();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ExplorerCore.Log($"Unable to patch {type.Name}.set_{property}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent setting non-UnityExplorer objects as selected when menu is open
|
||||
|
||||
public static bool Prefix_EventSystem_SetSelectedGameObject(GameObject __0)
|
||||
{
|
||||
if (!UIManager.ShowMenu || !UIManager.CanvasRoot)
|
||||
return true;
|
||||
|
||||
return __0 && __0.transform.root.gameObject.GetInstanceID() == UIManager.CanvasRoot.GetInstanceID();
|
||||
}
|
||||
|
||||
//public static bool Prefix_PointerInputModule_ClearSelection()
|
||||
//{
|
||||
// return !(UIManager.ShowMenu && UIManager.CanvasRoot);
|
||||
//}
|
||||
|
||||
// Force EventSystem.current to be UnityExplorer's when menu is open
|
||||
|
||||
public static void Prefix_EventSystem_set_current(ref EventSystem value)
|
||||
{
|
||||
if (!m_settingEventSystem)
|
||||
if (!settingEventSystem && value)
|
||||
{
|
||||
m_lastEventSystem = value;
|
||||
m_lastInputModule = value?.currentInputModule;
|
||||
lastEventSystem = value;
|
||||
lastInputModule = value.currentInputModule;
|
||||
}
|
||||
|
||||
if (ShouldActuallyUnlock)
|
||||
value = UIManager.EventSys;
|
||||
if (!UIManager.EventSys)
|
||||
return;
|
||||
|
||||
if (!settingEventSystem && ShouldActuallyUnlock && !ConfigManager.Disable_EventSystem_Override.Value)
|
||||
{
|
||||
value = UIManager.EventSys;
|
||||
value.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,9 +263,9 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public static void Prefix_set_lockState(ref CursorLockMode value)
|
||||
{
|
||||
if (!m_currentlySettingCursor)
|
||||
if (!currentlySettingCursor)
|
||||
{
|
||||
m_lastLockMode = value;
|
||||
lastLockMode = value;
|
||||
|
||||
if (ShouldActuallyUnlock)
|
||||
value = CursorLockMode.None;
|
||||
@ -179,13 +274,13 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public static void Prefix_set_visible(ref bool value)
|
||||
{
|
||||
if (!m_currentlySettingCursor)
|
||||
if (!currentlySettingCursor)
|
||||
{
|
||||
m_lastVisibleState = value;
|
||||
lastVisibleState = value;
|
||||
|
||||
if (ShouldActuallyUnlock)
|
||||
value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ namespace UnityExplorer.Core.Input
|
||||
public interface IHandleInput
|
||||
{
|
||||
Vector2 MousePosition { get; }
|
||||
Vector2 MouseScrollDelta { get; }
|
||||
|
||||
bool GetKeyDown(KeyCode key);
|
||||
bool GetKey(KeyCode key);
|
||||
@ -18,4 +19,4 @@ namespace UnityExplorer.Core.Input
|
||||
void AddUIInputModule();
|
||||
void ActivateModule();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UnityExplorer.Core.Input
|
||||
@ -20,14 +20,27 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public static Vector3 MousePosition => m_inputModule.MousePosition;
|
||||
|
||||
public static bool GetKeyDown(KeyCode key) => m_inputModule.GetKeyDown(key);
|
||||
public static bool GetKey(KeyCode key) => m_inputModule.GetKey(key);
|
||||
public static bool GetKeyDown(KeyCode key)
|
||||
{
|
||||
if (key == KeyCode.None)
|
||||
return false;
|
||||
return m_inputModule.GetKeyDown(key);
|
||||
}
|
||||
|
||||
public static bool GetKey(KeyCode key)
|
||||
{
|
||||
if (key == KeyCode.None)
|
||||
return false;
|
||||
return m_inputModule.GetKey(key);
|
||||
}
|
||||
|
||||
public static bool GetMouseButtonDown(int btn) => m_inputModule.GetMouseButtonDown(btn);
|
||||
public static bool GetMouseButton(int btn) => m_inputModule.GetMouseButton(btn);
|
||||
|
||||
public static BaseInputModule UIInput => m_inputModule.UIModule;
|
||||
|
||||
public static Vector2 MouseScrollDelta => m_inputModule.MouseScrollDelta;
|
||||
|
||||
public static void ActivateUIModule() => m_inputModule.ActivateModule();
|
||||
|
||||
public static void AddUIModule()
|
||||
@ -38,25 +51,53 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
if (InputSystem.TKeyboard != null || (ReflectionUtility.LoadModule("Unity.InputSystem") && InputSystem.TKeyboard != null))
|
||||
{
|
||||
m_inputModule = new InputSystem();
|
||||
CurrentType = InputType.InputSystem;
|
||||
}
|
||||
else if (LegacyInput.TInput != null || (ReflectionUtility.LoadModule("UnityEngine.InputLegacyModule") && LegacyInput.TInput != null))
|
||||
{
|
||||
m_inputModule = new LegacyInput();
|
||||
CurrentType = InputType.Legacy;
|
||||
}
|
||||
|
||||
if (m_inputModule == null)
|
||||
{
|
||||
ExplorerCore.LogWarning("Could not find any Input Module Type!");
|
||||
m_inputModule = new NoInput();
|
||||
CurrentType = InputType.None;
|
||||
}
|
||||
InitHandler();
|
||||
|
||||
CursorUnlocker.Init();
|
||||
}
|
||||
|
||||
private static void InitHandler()
|
||||
{
|
||||
// First, just try to use the legacy input, see if its working.
|
||||
// The InputSystem package may be present but not actually activated, so we can find out this way.
|
||||
|
||||
if (LegacyInput.TInput != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_inputModule = new LegacyInput();
|
||||
CurrentType = InputType.Legacy;
|
||||
|
||||
// make sure its working
|
||||
GetKeyDown(KeyCode.F5);
|
||||
|
||||
ExplorerCore.Log("Initialized Legacy Input support");
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// It's not working, we'll fall back to InputSystem.
|
||||
}
|
||||
}
|
||||
|
||||
if (InputSystem.TKeyboard != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_inputModule = new InputSystem();
|
||||
CurrentType = InputType.InputSystem;
|
||||
ExplorerCore.Log("Initialized new InputSystem support.");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
ExplorerCore.LogWarning("Could not find any Input Module Type!");
|
||||
m_inputModule = new NoInput();
|
||||
CurrentType = InputType.None;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityExplorer.Core.Unity;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityExplorer.UI;
|
||||
using System.Collections.Generic;
|
||||
using UnityExplorer.UI.Inspectors;
|
||||
#if CPP
|
||||
using UnhollowerRuntimeLib;
|
||||
#endif
|
||||
|
||||
namespace UnityExplorer.Core.Input
|
||||
{
|
||||
@ -16,7 +12,7 @@ namespace UnityExplorer.Core.Input
|
||||
{
|
||||
public InputSystem()
|
||||
{
|
||||
ExplorerCore.Log("Initializing new InputSystem support...");
|
||||
SetupSupportedDevices();
|
||||
|
||||
m_kbCurrentProp = TKeyboard.GetProperty("current");
|
||||
m_kbIndexer = TKeyboard.GetProperty("Item", new Type[] { TKey });
|
||||
@ -28,15 +24,48 @@ namespace UnityExplorer.Core.Input
|
||||
m_mouseCurrentProp = TMouse.GetProperty("current");
|
||||
m_leftButtonProp = TMouse.GetProperty("leftButton");
|
||||
m_rightButtonProp = TMouse.GetProperty("rightButton");
|
||||
m_scrollDeltaProp = TMouse.GetProperty("scroll");
|
||||
|
||||
m_positionProp = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Pointer")
|
||||
.GetProperty("position");
|
||||
|
||||
m_readVector2InputMethod = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl`1")
|
||||
ReadV2ControlMethod = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl`1")
|
||||
.MakeGenericType(typeof(Vector2))
|
||||
.GetMethod("ReadValue");
|
||||
}
|
||||
|
||||
internal static void SetupSupportedDevices()
|
||||
{
|
||||
try
|
||||
{
|
||||
// typeof(InputSystem)
|
||||
Type TInputSystem = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputSystem");
|
||||
// InputSystem.settings
|
||||
var settings = TInputSystem.GetProperty("settings", BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
|
||||
// typeof(InputSettings)
|
||||
Type TSettings = settings.GetActualType();
|
||||
// InputSettings.supportedDevices
|
||||
PropertyInfo supportedProp = TSettings.GetProperty("supportedDevices", BindingFlags.Public | BindingFlags.Instance);
|
||||
var supportedDevices = supportedProp.GetValue(settings, null);
|
||||
// An empty supportedDevices list means all devices are supported.
|
||||
#if CPP
|
||||
// weird hack for il2cpp, use the implicit operator and cast Il2CppStringArray to ReadOnlyArray<string>
|
||||
var args = new object[] { new UnhollowerBaseLib.Il2CppStringArray(0) };
|
||||
var method = supportedDevices.GetActualType().GetMethod("op_Implicit", BindingFlags.Static | BindingFlags.Public);
|
||||
supportedProp.SetValue(settings, method.Invoke(null, args), null);
|
||||
#else
|
||||
supportedProp.SetValue(settings, Activator.CreateInstance(supportedDevices.GetActualType(), new object[] { new string[0] }), null);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting up InputSystem.settings.supportedDevices list!");
|
||||
ExplorerCore.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#region reflection cache
|
||||
|
||||
public static Type TKeyboard => m_tKeyboard ?? (m_tKeyboard = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Keyboard"));
|
||||
private static Type m_tKeyboard;
|
||||
|
||||
@ -66,10 +95,17 @@ namespace UnityExplorer.Core.Input
|
||||
private static object m_rmb;
|
||||
private static PropertyInfo m_rightButtonProp;
|
||||
|
||||
private static MethodInfo ReadV2ControlMethod;
|
||||
|
||||
private static object MousePositionInfo => m_pos ?? (m_pos = m_positionProp.GetValue(CurrentMouse, null));
|
||||
private static object m_pos;
|
||||
private static PropertyInfo m_positionProp;
|
||||
private static MethodInfo m_readVector2InputMethod;
|
||||
|
||||
private static object MouseScrollInfo => m_scrollInfo ?? (m_scrollInfo = m_scrollDeltaProp.GetValue(CurrentMouse, null));
|
||||
private static object m_scrollInfo;
|
||||
private static PropertyInfo m_scrollDeltaProp;
|
||||
|
||||
#endregion
|
||||
|
||||
public Vector2 MousePosition
|
||||
{
|
||||
@ -77,26 +113,47 @@ namespace UnityExplorer.Core.Input
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Vector2)m_readVector2InputMethod.Invoke(MousePositionInfo, new object[0]);
|
||||
return (Vector2)ReadV2ControlMethod.Invoke(MousePositionInfo, ArgumentUtility.EmptyArgs);
|
||||
}
|
||||
catch
|
||||
catch { return Vector2.zero; }
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 MouseScrollDelta
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return Vector2.zero;
|
||||
return (Vector2)ReadV2ControlMethod.Invoke(MouseScrollInfo, ArgumentUtility.EmptyArgs);
|
||||
}
|
||||
catch { return Vector2.zero; }
|
||||
}
|
||||
}
|
||||
|
||||
internal static Dictionary<KeyCode, object> ActualKeyDict = new Dictionary<KeyCode, object>();
|
||||
internal static Dictionary<string, string> enumNameFixes = new Dictionary<string, string>
|
||||
{
|
||||
{ "Control", "Ctrl" },
|
||||
{ "Return", "Enter" },
|
||||
{ "Alpha", "Digit" },
|
||||
{ "Keypad", "Numpad" },
|
||||
{ "Numlock", "NumLock" },
|
||||
{ "Print", "PrintScreen" },
|
||||
{ "BackQuote", "Backquote" }
|
||||
};
|
||||
|
||||
internal object GetActualKey(KeyCode key)
|
||||
{
|
||||
if (!ActualKeyDict.ContainsKey(key))
|
||||
{
|
||||
var s = key.ToString();
|
||||
if (s.Contains("Control"))
|
||||
s = s.Replace("Control", "Ctrl");
|
||||
else if (s.Contains("Return"))
|
||||
s = "Enter";
|
||||
try
|
||||
{
|
||||
if (enumNameFixes.First(it => s.Contains(it.Key)) is KeyValuePair<string, string> entry)
|
||||
s = s.Replace(entry.Key, entry.Value);
|
||||
}
|
||||
catch { }
|
||||
|
||||
var parsed = Enum.Parse(TKey, s);
|
||||
var actualKey = m_kbIndexer.GetValue(CurrentKeyboard, new object[] { parsed });
|
||||
@ -113,6 +170,8 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public bool GetMouseButtonDown(int btn)
|
||||
{
|
||||
if (CurrentMouse == null)
|
||||
return false;
|
||||
switch (btn)
|
||||
{
|
||||
case 0: return (bool)m_btnWasPressedProp.GetValue(LeftMouseButton, null);
|
||||
@ -124,6 +183,8 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public bool GetMouseButton(int btn)
|
||||
{
|
||||
if (CurrentMouse == null)
|
||||
return false;
|
||||
switch (btn)
|
||||
{
|
||||
case 0: return (bool)m_btnIsPressedProp.GetValue(LeftMouseButton, null);
|
||||
@ -152,17 +213,15 @@ namespace UnityExplorer.Core.Input
|
||||
}
|
||||
|
||||
var assetType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionAsset");
|
||||
#if CPP
|
||||
m_newInputModule = UIManager.CanvasRoot.AddComponent(Il2CppType.From(TInputSystemUIInputModule)).TryCast<BaseInputModule>();
|
||||
var asset = ScriptableObject.CreateInstance(Il2CppType.From(assetType));
|
||||
#else
|
||||
m_newInputModule = (BaseInputModule)UIManager.CanvasRoot.AddComponent(TInputSystemUIInputModule);
|
||||
var asset = ScriptableObject.CreateInstance(assetType);
|
||||
#endif
|
||||
m_newInputModule = RuntimeProvider.Instance.AddComponent<BaseInputModule>(UIManager.CanvasRoot, TInputSystemUIInputModule);
|
||||
var asset = RuntimeProvider.Instance.CreateScriptable(assetType)
|
||||
.TryCast(assetType);
|
||||
|
||||
inputExtensions = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionSetupExtensions");
|
||||
|
||||
var addMap = inputExtensions.GetMethod("AddActionMap", new Type[] { assetType, typeof(string) });
|
||||
var map = addMap.Invoke(null, new object[] { asset, "UI" });
|
||||
var map = addMap.Invoke(null, new object[] { asset, "UI" })
|
||||
.TryCast(ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionMap"));
|
||||
|
||||
CreateAction(map, "point", new[] { "<Mouse>/position" }, "point");
|
||||
CreateAction(map, "click", new[] { "<Mouse>/leftButton" }, "leftClick");
|
||||
@ -170,7 +229,7 @@ namespace UnityExplorer.Core.Input
|
||||
CreateAction(map, "scrollWheel", new[] { "<Mouse>/scroll" }, "scrollWheel");
|
||||
|
||||
UI_Enable = map.GetType().GetMethod("Enable");
|
||||
UI_Enable.Invoke(map, new object[0]);
|
||||
UI_Enable.Invoke(map, ArgumentUtility.EmptyArgs);
|
||||
UI_ActionMap = map;
|
||||
}
|
||||
|
||||
@ -180,29 +239,31 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
private void CreateAction(object map, string actionName, string[] bindings, string propertyName)
|
||||
{
|
||||
var inputActionType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputAction");
|
||||
var addAction = inputExtensions.GetMethod("AddAction");
|
||||
var pointAction = addAction.Invoke(null, new object[] { map, actionName, default, null, null, null, null, null });
|
||||
var action = addAction.Invoke(null, new object[] { map, actionName, default, null, null, null, null, null })
|
||||
.TryCast(inputActionType);
|
||||
|
||||
var inputActionType = pointAction.GetType();
|
||||
var addBinding = inputExtensions.GetMethod("AddBinding",
|
||||
new Type[] { inputActionType, typeof(string), typeof(string), typeof(string), typeof(string) });
|
||||
|
||||
foreach (string binding in bindings)
|
||||
addBinding.Invoke(null, new object[] { pointAction, binding, null, null, null });
|
||||
addBinding.Invoke(null, new object[] { action.TryCast(inputActionType), binding, null, null, null });
|
||||
|
||||
var inputRef = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionReference")
|
||||
.GetMethod("Create")
|
||||
.Invoke(null, new object[] { pointAction });
|
||||
var refType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputActionReference");
|
||||
var inputRef = refType.GetMethod("Create")
|
||||
.Invoke(null, new object[] { action })
|
||||
.TryCast(refType);
|
||||
|
||||
TInputSystemUIInputModule
|
||||
.GetProperty(propertyName)
|
||||
.SetValue(m_newInputModule, inputRef, null);
|
||||
.SetValue(m_newInputModule.TryCast(TInputSystemUIInputModule), inputRef, null);
|
||||
}
|
||||
|
||||
public void ActivateModule()
|
||||
{
|
||||
m_newInputModule.ActivateModule();
|
||||
UI_Enable.Invoke(UI_ActionMap, new object[0]);
|
||||
UI_Enable.Invoke(UI_ActionMap, ArgumentUtility.EmptyArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityExplorer.Core.Unity;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityExplorer.UI;
|
||||
@ -11,9 +10,8 @@ namespace UnityExplorer.Core.Input
|
||||
{
|
||||
public LegacyInput()
|
||||
{
|
||||
ExplorerCore.Log("Initializing Legacy Input support...");
|
||||
|
||||
m_mousePositionProp = TInput.GetProperty("mousePosition");
|
||||
m_mouseDeltaProp = TInput.GetProperty("mouseScrollDelta");
|
||||
m_getKeyMethod = TInput.GetMethod("GetKey", new Type[] { typeof(KeyCode) });
|
||||
m_getKeyDownMethod = TInput.GetMethod("GetKeyDown", new Type[] { typeof(KeyCode) });
|
||||
m_getMouseButtonMethod = TInput.GetMethod("GetMouseButton", new Type[] { typeof(int) });
|
||||
@ -24,6 +22,7 @@ namespace UnityExplorer.Core.Input
|
||||
private static Type m_tInput;
|
||||
|
||||
private static PropertyInfo m_mousePositionProp;
|
||||
private static PropertyInfo m_mouseDeltaProp;
|
||||
private static MethodInfo m_getKeyMethod;
|
||||
private static MethodInfo m_getKeyDownMethod;
|
||||
private static MethodInfo m_getMouseButtonMethod;
|
||||
@ -31,6 +30,8 @@ namespace UnityExplorer.Core.Input
|
||||
|
||||
public Vector2 MousePosition => (Vector3)m_mousePositionProp.GetValue(null, null);
|
||||
|
||||
public Vector2 MouseScrollDelta => (Vector2)m_mouseDeltaProp.GetValue(null, null);
|
||||
|
||||
public bool GetKey(KeyCode key) => (bool)m_getKeyMethod.Invoke(null, new object[] { key });
|
||||
|
||||
public bool GetKeyDown(KeyCode key) => (bool)m_getKeyDownMethod.Invoke(null, new object[] { key });
|
||||
@ -54,4 +55,4 @@ namespace UnityExplorer.Core.Input
|
||||
m_inputModule.ActivateModule();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ namespace UnityExplorer.Core.Input
|
||||
public class NoInput : IHandleInput
|
||||
{
|
||||
public Vector2 MousePosition => Vector2.zero;
|
||||
public Vector2 MouseScrollDelta => Vector2.zero;
|
||||
|
||||
public bool GetKey(KeyCode key) => false;
|
||||
public bool GetKeyDown(KeyCode key) => false;
|
||||
@ -19,4 +20,4 @@ namespace UnityExplorer.Core.Input
|
||||
public void ActivateModule() { }
|
||||
public void AddUIInputModule() { }
|
||||
}
|
||||
}
|
||||
}
|
113
src/Core/Reflection/Extensions.cs
Normal file
113
src/Core/Reflection/Extensions.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
// ReflectionUtility extensions
|
||||
|
||||
public static Type GetActualType(this object obj)
|
||||
=> ReflectionUtility.Instance.Internal_GetActualType(obj);
|
||||
|
||||
public static object TryCast(this object obj)
|
||||
=> ReflectionUtility.Instance.Internal_TryCast(obj, ReflectionUtility.Instance.Internal_GetActualType(obj));
|
||||
|
||||
public static object TryCast(this object obj, Type castTo)
|
||||
=> ReflectionUtility.Instance.Internal_TryCast(obj, castTo);
|
||||
|
||||
public static T TryCast<T>(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (T)ReflectionUtility.Instance.Internal_TryCast(obj, typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// ------- Misc extensions --------
|
||||
|
||||
/// <summary>
|
||||
/// Safely try to get all Types inside an Assembly.
|
||||
/// </summary>
|
||||
public static IEnumerable<Type> TryGetTypes(this Assembly asm)
|
||||
{
|
||||
try
|
||||
{
|
||||
return asm.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
try
|
||||
{
|
||||
return asm.GetExportedTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return e.Types.Where(t => t != null);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<Type>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if the two objects are reference-equal, including checking for UnityEngine.Object-equality and Il2CppSystem.Object-equality.
|
||||
/// </summary>
|
||||
public static bool ReferenceEqual(this object objA, object objB)
|
||||
{
|
||||
if (object.ReferenceEquals(objA, objB))
|
||||
return true;
|
||||
|
||||
if (objA is UnityEngine.Object unityA && objB is UnityEngine.Object unityB)
|
||||
{
|
||||
if (unityA && unityB && unityA.m_CachedPtr == unityB.m_CachedPtr)
|
||||
return true;
|
||||
}
|
||||
|
||||
#if CPP
|
||||
if (objA is Il2CppSystem.Object cppA && objB is Il2CppSystem.Object cppB
|
||||
&& cppA.Pointer == cppB.Pointer)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception.
|
||||
/// </summary>
|
||||
public static string ReflectionExToString(this Exception e, bool innerMost = true)
|
||||
{
|
||||
if (innerMost)
|
||||
e = e.GetInnerMostException();
|
||||
|
||||
return $"{e.GetType()}: {e.Message}";
|
||||
}
|
||||
|
||||
public static Exception GetInnerMostException(this Exception e)
|
||||
{
|
||||
while (e != null)
|
||||
{
|
||||
if (e.InnerException == null)
|
||||
break;
|
||||
#if CPP
|
||||
if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException)
|
||||
break;
|
||||
#endif
|
||||
e = e.InnerException;
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
956
src/Core/Reflection/Il2CppReflection.cs
Normal file
956
src/Core/Reflection/Il2CppReflection.cs
Normal file
@ -0,0 +1,956 @@
|
||||
#if CPP
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnhollowerBaseLib;
|
||||
using UnhollowerRuntimeLib;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using UnityExplorer.Core;
|
||||
using CppType = Il2CppSystem.Type;
|
||||
using BF = System.Reflection.BindingFlags;
|
||||
using UnhollowerBaseLib.Attributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
public class Il2CppReflection : ReflectionUtility
|
||||
{
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
float start = Time.realtimeSinceStartup;
|
||||
TryLoadGameModules();
|
||||
ExplorerCore.Log($"Loaded Unhollowed modules in {Time.realtimeSinceStartup - start} seconds");
|
||||
|
||||
start = Time.realtimeSinceStartup;
|
||||
BuildDeobfuscationCache();
|
||||
OnTypeLoaded += TryCacheDeobfuscatedType;
|
||||
ExplorerCore.Log($"Setup IL2CPP reflection in {Time.realtimeSinceStartup - start} seconds, " +
|
||||
$"deobfuscated types count: {DeobfuscatedTypes.Count}");
|
||||
}
|
||||
|
||||
#region IL2CPP Extern and pointers
|
||||
|
||||
// Extern C++ methods
|
||||
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass);
|
||||
|
||||
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern IntPtr il2cpp_object_get_class(IntPtr obj);
|
||||
|
||||
public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _);
|
||||
|
||||
public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr)
|
||||
{
|
||||
if (!cppClassPointers.TryGetValue(type.AssemblyQualifiedName, out il2cppPtr))
|
||||
{
|
||||
il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>)
|
||||
.MakeGenericType(new Type[] { type })
|
||||
.GetField("NativeClassPtr", BF.Public | BF.Static)
|
||||
.GetValue(null);
|
||||
|
||||
cppClassPointers.Add(type.AssemblyQualifiedName, il2cppPtr);
|
||||
}
|
||||
|
||||
return il2cppPtr != IntPtr.Zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Deobfuscation cache
|
||||
|
||||
private static readonly Dictionary<string, Type> DeobfuscatedTypes = new Dictionary<string, Type>();
|
||||
private static readonly Dictionary<string, string> reverseDeobCache = new Dictionary<string, string>();
|
||||
|
||||
private static void BuildDeobfuscationCache()
|
||||
{
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
foreach (var type in asm.TryGetTypes())
|
||||
TryCacheDeobfuscatedType(type);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryCacheDeobfuscatedType(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!type.CustomAttributes.Any())
|
||||
return;
|
||||
|
||||
foreach (var att in type.CustomAttributes)
|
||||
{
|
||||
// Thanks to Slaynash for this
|
||||
|
||||
if (att.AttributeType == typeof(ObfuscatedNameAttribute))
|
||||
{
|
||||
string obfuscatedName = att.ConstructorArguments[0].Value.ToString();
|
||||
|
||||
DeobfuscatedTypes.Add(obfuscatedName, type);
|
||||
reverseDeobCache.Add(type.FullName, obfuscatedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
internal override string Internal_ProcessTypeInString(string theString, Type type)
|
||||
{
|
||||
if (reverseDeobCache.TryGetValue(type.FullName, out string obName))
|
||||
return theString.Replace(obName, type.FullName);
|
||||
|
||||
return theString;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
// Get type by name
|
||||
|
||||
internal override Type Internal_GetTypeByName(string fullName)
|
||||
{
|
||||
if (DeobfuscatedTypes.TryGetValue(fullName, out Type deob))
|
||||
return deob;
|
||||
|
||||
return base.Internal_GetTypeByName(fullName);
|
||||
}
|
||||
|
||||
#region Get actual type
|
||||
|
||||
internal override Type Internal_GetActualType(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var type = obj.GetType();
|
||||
try
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
return type;
|
||||
|
||||
if (IsString(obj))
|
||||
return typeof(string);
|
||||
|
||||
if (IsIl2CppPrimitive(type))
|
||||
return il2cppPrimitivesToMono[type.FullName];
|
||||
|
||||
if (obj is Il2CppSystem.Object cppObject)
|
||||
{
|
||||
var cppType = cppObject.GetIl2CppType();
|
||||
|
||||
// check if type is injected
|
||||
IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer);
|
||||
if (RuntimeSpecificsStore.IsInjected(classPtr))
|
||||
{
|
||||
// Note: This will fail on injected subclasses.
|
||||
// - {Namespace}.{Class}.{Subclass} would be {Namespace}.{Subclass} when injected.
|
||||
// Not sure on solution yet.
|
||||
return GetTypeByName(cppType.FullName) ?? type;
|
||||
}
|
||||
|
||||
if (AllTypes.TryGetValue(cppType.FullName, out Type primitive) && primitive.IsPrimitive)
|
||||
return primitive;
|
||||
|
||||
return GetUnhollowedType(cppType) ?? type;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception in IL2CPP GetActualType: " + ex);
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
public static Type GetUnhollowedType(CppType cppType)
|
||||
{
|
||||
var fullname = cppType.FullName;
|
||||
|
||||
if (DeobfuscatedTypes.TryGetValue(fullname, out Type deob))
|
||||
return deob;
|
||||
|
||||
if (fullname.StartsWith("System."))
|
||||
fullname = $"Il2Cpp{fullname}";
|
||||
|
||||
if (!AllTypes.TryGetValue(fullname, out Type monoType))
|
||||
ExplorerCore.LogWarning($"Failed to get type by name '{fullname}'!");
|
||||
return monoType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Casting
|
||||
|
||||
private static readonly Dictionary<string, IntPtr> cppClassPointers = new Dictionary<string, IntPtr>();
|
||||
|
||||
internal override object Internal_TryCast(object obj, Type castTo)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var type = obj.GetType();
|
||||
|
||||
if (type == castTo)
|
||||
return obj;
|
||||
|
||||
// from structs
|
||||
if (type.IsValueType)
|
||||
{
|
||||
// from il2cpp primitive to system primitive
|
||||
if (IsIl2CppPrimitive(type) && castTo.IsPrimitive)
|
||||
{
|
||||
return MakeMonoPrimitive(obj);
|
||||
}
|
||||
// from system primitive to il2cpp primitive
|
||||
else if (IsIl2CppPrimitive(castTo))
|
||||
{
|
||||
return MakeIl2CppPrimitive(castTo, obj);
|
||||
}
|
||||
// from other structs to il2cpp object
|
||||
else if (typeof(Il2CppSystem.Object).IsAssignableFrom(castTo))
|
||||
{
|
||||
return BoxIl2CppObject(obj).TryCast(castTo);
|
||||
}
|
||||
else
|
||||
return obj;
|
||||
}
|
||||
|
||||
// from string to il2cpp.Object / il2cpp.String
|
||||
if (obj is string && typeof(Il2CppSystem.Object).IsAssignableFrom(castTo))
|
||||
{
|
||||
return BoxStringToType(obj, castTo);
|
||||
}
|
||||
|
||||
// from il2cpp objects...
|
||||
|
||||
if (!(obj is Il2CppObjectBase cppObj))
|
||||
return obj;
|
||||
|
||||
// from Il2CppSystem.Object to a struct
|
||||
if (castTo.IsValueType)
|
||||
return UnboxCppObject(cppObj, castTo);
|
||||
// or to system string
|
||||
else if (castTo == typeof(string))
|
||||
return UnboxString(obj);
|
||||
|
||||
if (!Il2CppTypeNotNull(castTo, out IntPtr castToPtr))
|
||||
return obj;
|
||||
|
||||
// Casting from il2cpp object to il2cpp object...
|
||||
|
||||
IntPtr castFromPtr = il2cpp_object_get_class(cppObj.Pointer);
|
||||
|
||||
if (!il2cpp_class_is_assignable_from(castToPtr, castFromPtr))
|
||||
return null;
|
||||
|
||||
if (RuntimeSpecificsStore.IsInjected(castToPtr))
|
||||
{
|
||||
var injectedObj = UnhollowerBaseLib.Runtime.ClassInjectorBase.GetMonoObjectFromIl2CppPointer(cppObj.Pointer);
|
||||
return injectedObj ?? obj;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance(castTo, cppObj.Pointer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
//private static bool IsAssignableFrom(Type thisType, Type fromType)
|
||||
//{
|
||||
// if (!Il2CppTypeNotNull(fromType, out IntPtr fromTypePtr)
|
||||
// || !Il2CppTypeNotNull(thisType, out IntPtr thisTypePtr))
|
||||
// {
|
||||
// // one or both of the types are not Il2Cpp types, use normal check
|
||||
// return thisType.IsAssignableFrom(fromType);
|
||||
// }
|
||||
//
|
||||
// return il2cpp_class_is_assignable_from(thisTypePtr, fromTypePtr);
|
||||
//}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Boxing and unboxing ValueTypes
|
||||
|
||||
// cached il2cpp unbox methods
|
||||
internal static readonly Dictionary<string, MethodInfo> unboxMethods = new Dictionary<string, MethodInfo>();
|
||||
|
||||
// Unbox an il2cpp object to a struct or System primitive.
|
||||
public object UnboxCppObject(Il2CppObjectBase cppObj, Type toType)
|
||||
{
|
||||
if (!toType.IsValueType)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (toType.IsEnum)
|
||||
{
|
||||
// Check for nullable enums
|
||||
var type = cppObj.GetType();
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Il2CppSystem.Nullable<>))
|
||||
{
|
||||
var nullable = cppObj.TryCast(type);
|
||||
var nullableHasValueProperty = type.GetProperty("HasValue");
|
||||
if ((bool)nullableHasValueProperty.GetValue(nullable, null))
|
||||
{
|
||||
// nullable has a value.
|
||||
var nullableValueProperty = type.GetProperty("Value");
|
||||
return Enum.Parse(toType, nullableValueProperty.GetValue(nullable, null).ToString());
|
||||
}
|
||||
// nullable and no current value.
|
||||
return cppObj;
|
||||
}
|
||||
|
||||
return Enum.Parse(toType, cppObj.ToString());
|
||||
}
|
||||
|
||||
// Not enum, unbox with Il2CppObjectBase.Unbox
|
||||
|
||||
var name = toType.AssemblyQualifiedName;
|
||||
|
||||
if (!unboxMethods.ContainsKey(name))
|
||||
{
|
||||
unboxMethods.Add(name, typeof(Il2CppObjectBase)
|
||||
.GetMethod("Unbox")
|
||||
.MakeGenericMethod(toType));
|
||||
}
|
||||
|
||||
return unboxMethods[name].Invoke(cppObj, ArgumentUtility.EmptyArgs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception Unboxing Il2Cpp object to struct: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Il2CppSystem.Object BoxIl2CppObject(object cppStruct, Type structType)
|
||||
{
|
||||
return GetMethodInfo(structType, "BoxIl2CppObject", ArgumentUtility.EmptyTypes)
|
||||
.Invoke(cppStruct, ArgumentUtility.EmptyArgs)
|
||||
as Il2CppSystem.Object;
|
||||
}
|
||||
|
||||
public Il2CppSystem.Object BoxIl2CppObject(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var type = value.GetType();
|
||||
if (!type.IsValueType)
|
||||
return null;
|
||||
|
||||
if (type.IsEnum)
|
||||
return Il2CppSystem.Enum.Parse(Il2CppType.From(type), value.ToString());
|
||||
|
||||
if (type.IsPrimitive && AllTypes.TryGetValue($"Il2Cpp{type.FullName}", out Type cppType))
|
||||
return BoxIl2CppObject(MakeIl2CppPrimitive(cppType, value), cppType);
|
||||
|
||||
return BoxIl2CppObject(value, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception in BoxIl2CppObject: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers for Il2Cpp primitive <-> Mono
|
||||
|
||||
internal static readonly Dictionary<string, Type> il2cppPrimitivesToMono = new Dictionary<string, Type>
|
||||
{
|
||||
{ "Il2CppSystem.Boolean", typeof(bool) },
|
||||
{ "Il2CppSystem.Byte", typeof(byte) },
|
||||
{ "Il2CppSystem.SByte", typeof(sbyte) },
|
||||
{ "Il2CppSystem.Char", typeof(char) },
|
||||
{ "Il2CppSystem.Double", typeof(double) },
|
||||
{ "Il2CppSystem.Single", typeof(float) },
|
||||
{ "Il2CppSystem.Int32", typeof(int) },
|
||||
{ "Il2CppSystem.UInt32", typeof(uint) },
|
||||
{ "Il2CppSystem.Int64", typeof(long) },
|
||||
{ "Il2CppSystem.UInt64", typeof(ulong) },
|
||||
{ "Il2CppSystem.Int16", typeof(short) },
|
||||
{ "Il2CppSystem.UInt16", typeof(ushort) },
|
||||
{ "Il2CppSystem.IntPtr", typeof(IntPtr) },
|
||||
{ "Il2CppSystem.UIntPtr", typeof(UIntPtr) }
|
||||
};
|
||||
|
||||
public static bool IsIl2CppPrimitive(object obj) => IsIl2CppPrimitive(obj.GetType());
|
||||
|
||||
public static bool IsIl2CppPrimitive(Type type) => il2cppPrimitivesToMono.ContainsKey(type.FullName);
|
||||
|
||||
public object MakeMonoPrimitive(object cppPrimitive)
|
||||
{
|
||||
return GetFieldInfo(cppPrimitive.GetType(), "m_value").GetValue(cppPrimitive);
|
||||
}
|
||||
|
||||
public object MakeIl2CppPrimitive(Type cppType, object monoValue)
|
||||
{
|
||||
var cppStruct = Activator.CreateInstance(cppType);
|
||||
GetFieldInfo(cppType, "m_value").SetValue(cppStruct, monoValue);
|
||||
return cppStruct;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region String boxing/unboxing
|
||||
|
||||
private const string IL2CPP_STRING_FULLNAME = "Il2CppSystem.String";
|
||||
private const string STRING_FULLNAME = "System.String";
|
||||
|
||||
public bool IsString(object obj)
|
||||
{
|
||||
if (obj is string || obj is Il2CppSystem.String)
|
||||
return true;
|
||||
|
||||
if (obj is Il2CppSystem.Object cppObj)
|
||||
{
|
||||
var type = cppObj.GetIl2CppType();
|
||||
return type.FullName == IL2CPP_STRING_FULLNAME || type.FullName == STRING_FULLNAME;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public object BoxStringToType(object value, Type castTo)
|
||||
{
|
||||
if (castTo == typeof(Il2CppSystem.String))
|
||||
return (Il2CppSystem.String)(value as string);
|
||||
else
|
||||
return (Il2CppSystem.Object)(value as string);
|
||||
}
|
||||
|
||||
public string UnboxString(object value)
|
||||
{
|
||||
if (value is string s)
|
||||
return s;
|
||||
|
||||
s = null;
|
||||
if (value is Il2CppSystem.Object cppObject)
|
||||
s = cppObject.ToString();
|
||||
else if (value is Il2CppSystem.String cppString)
|
||||
s = cppString;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Singleton finder
|
||||
|
||||
internal override void Internal_FindSingleton(string[] possibleNames, Type type, BF flags, List<object> instances)
|
||||
{
|
||||
PropertyInfo pi;
|
||||
foreach (var name in possibleNames)
|
||||
{
|
||||
pi = type.GetProperty(name, flags);
|
||||
if (pi != null)
|
||||
{
|
||||
var instance = pi.GetValue(null, null);
|
||||
if (instance != null)
|
||||
{
|
||||
instances.Add(instance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.Internal_FindSingleton(possibleNames, type, flags, instances);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Force-loading game modules
|
||||
|
||||
// Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded.
|
||||
|
||||
// Force loading all il2cpp modules
|
||||
|
||||
internal void TryLoadGameModules()
|
||||
{
|
||||
var dir = ExplorerCore.Loader.UnhollowedModulesFolder;
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
foreach (var filePath in Directory.GetFiles(dir, "*.dll"))
|
||||
DoLoadModule(filePath);
|
||||
}
|
||||
else
|
||||
ExplorerCore.LogWarning($"Expected Unhollowed folder path does not exist: '{dir}'. " +
|
||||
$"If you are using the standalone release, you can specify the Unhollowed modules path when you call CreateInstance().");
|
||||
}
|
||||
|
||||
internal bool DoLoadModule(string fullPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullPath) || !File.Exists(fullPath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Assembly.LoadFile(fullPath);
|
||||
return true;
|
||||
}
|
||||
catch //(Exception e)
|
||||
{
|
||||
//ExplorerCore.LogWarning($"Failed loading module '{Path.GetFileName(fullPath)}'! {e.ReflectionExToString()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Il2cpp reflection blacklist
|
||||
|
||||
public override string[] DefaultReflectionBlacklist => defaultIl2CppBlacklist.ToArray();
|
||||
|
||||
// These methods currently cause a crash in most il2cpp games,
|
||||
// even from doing "GetParameters()" on the MemberInfo.
|
||||
// Blacklisting until the issue is fixed in Unhollower.
|
||||
public static HashSet<string> defaultIl2CppBlacklist = new HashSet<string>
|
||||
{
|
||||
// These were deprecated a long time ago, still show up in some IL2CPP games for some reason
|
||||
"UnityEngine.MonoBehaviour.allowPrefabModeInPlayMode",
|
||||
"UnityEngine.MonoBehaviour.runInEditMode",
|
||||
"UnityEngine.Component.animation",
|
||||
"UnityEngine.Component.audio",
|
||||
"UnityEngine.Component.camera",
|
||||
"UnityEngine.Component.collider",
|
||||
"UnityEngine.Component.collider2D",
|
||||
"UnityEngine.Component.constantForce",
|
||||
"UnityEngine.Component.hingeJoint",
|
||||
"UnityEngine.Component.light",
|
||||
"UnityEngine.Component.networkView",
|
||||
"UnityEngine.Component.particleSystem",
|
||||
"UnityEngine.Component.renderer",
|
||||
"UnityEngine.Component.rigidbody",
|
||||
"UnityEngine.Component.rigidbody2D",
|
||||
"UnityEngine.Light.flare",
|
||||
// These can cause a crash in IL2CPP
|
||||
"Il2CppSystem.Type.DeclaringMethod",
|
||||
"Il2CppSystem.RuntimeType.DeclaringMethod",
|
||||
"Unity.Jobs.LowLevel.Unsafe.JobsUtility.CreateJobReflectionData",
|
||||
"Unity.Profiling.ProfilerRecorder.CopyTo",
|
||||
"Unity.Profiling.ProfilerRecorder.StartNew",
|
||||
"UnityEngine.Analytics.Analytics.RegisterEvent",
|
||||
"UnityEngine.Analytics.Analytics.SendEvent",
|
||||
"UnityEngine.Analytics.ContinuousEvent+ConfigureEventDelegate.Invoke",
|
||||
"UnityEngine.Analytics.ContinuousEvent.ConfigureEvent",
|
||||
"UnityEngine.Animations.AnimationLayerMixerPlayable.Create",
|
||||
"UnityEngine.Animations.AnimationLayerMixerPlayable.CreateHandle",
|
||||
"UnityEngine.Animations.AnimationMixerPlayable.Create",
|
||||
"UnityEngine.Animations.AnimationMixerPlayable.CreateHandle",
|
||||
"UnityEngine.AssetBundle.RecompressAssetBundleAsync",
|
||||
"UnityEngine.Audio.AudioMixerPlayable.Create",
|
||||
"UnityEngine.BoxcastCommand.ScheduleBatch",
|
||||
"UnityEngine.Camera.CalculateProjectionMatrixFromPhysicalProperties",
|
||||
"UnityEngine.Canvas.renderingDisplaySize",
|
||||
"UnityEngine.CapsulecastCommand.ScheduleBatch",
|
||||
"UnityEngine.Collider2D.Cast",
|
||||
"UnityEngine.Collider2D.Raycast",
|
||||
"UnityEngine.ComputeBuffer+BeginBufferWriteDelegate.Invoke",
|
||||
"UnityEngine.ComputeBuffer+EndBufferWriteDelegate.Invoke",
|
||||
"UnityEngine.ComputeBuffer.BeginBufferWrite",
|
||||
"UnityEngine.ComputeBuffer.EndBufferWrite",
|
||||
"UnityEngine.Cubemap+SetPixelDataImplArrayDelegate.Invoke",
|
||||
"UnityEngine.Cubemap+SetPixelDataImplDelegate.Invoke",
|
||||
"UnityEngine.Cubemap.SetPixelDataImpl",
|
||||
"UnityEngine.Cubemap.SetPixelDataImplArray",
|
||||
"UnityEngine.CubemapArray+SetPixelDataImplArrayDelegate.Invoke",
|
||||
"UnityEngine.CubemapArray+SetPixelDataImplDelegate.Invoke",
|
||||
"UnityEngine.CubemapArray.SetPixelDataImpl",
|
||||
"UnityEngine.CubemapArray.SetPixelDataImplArray",
|
||||
"UnityEngine.Experimental.Playables.MaterialEffectPlayable.Create",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure+AddInstanceDelegate.Invoke",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure+AddInstance_Procedural_InjectedDelegate.Invoke",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance_Procedural",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingAccelerationStructure.AddInstance_Procedural_Injected",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingShader+DispatchDelegate.Invoke",
|
||||
"UnityEngine.Experimental.Rendering.RayTracingShader.Dispatch",
|
||||
"UnityEngine.Experimental.Rendering.RenderPassAttachment.Clear",
|
||||
"UnityEngine.GUI.DoButtonGrid",
|
||||
"UnityEngine.GUI.Slider",
|
||||
"UnityEngine.GUI.Toolbar",
|
||||
"UnityEngine.Graphics.DrawMeshInstancedIndirect",
|
||||
"UnityEngine.Graphics.DrawMeshInstancedProcedural",
|
||||
"UnityEngine.Graphics.DrawProcedural",
|
||||
"UnityEngine.Graphics.DrawProceduralIndirect",
|
||||
"UnityEngine.Graphics.DrawProceduralIndirectNow",
|
||||
"UnityEngine.Graphics.DrawProceduralNow",
|
||||
"UnityEngine.LineRenderer+BakeMeshDelegate.Invoke",
|
||||
"UnityEngine.LineRenderer.BakeMesh",
|
||||
"UnityEngine.Mesh.GetIndices",
|
||||
"UnityEngine.Mesh.GetTriangles",
|
||||
"UnityEngine.Mesh.SetIndices",
|
||||
"UnityEngine.Mesh.SetTriangles",
|
||||
"UnityEngine.Physics2D.BoxCast",
|
||||
"UnityEngine.Physics2D.CapsuleCast",
|
||||
"UnityEngine.Physics2D.CircleCast",
|
||||
"UnityEngine.PhysicsScene.BoxCast",
|
||||
"UnityEngine.PhysicsScene.CapsuleCast",
|
||||
"UnityEngine.PhysicsScene.OverlapBox",
|
||||
"UnityEngine.PhysicsScene.OverlapCapsule",
|
||||
"UnityEngine.PhysicsScene.SphereCast",
|
||||
"UnityEngine.PhysicsScene2D.BoxCast",
|
||||
"UnityEngine.PhysicsScene2D.CapsuleCast",
|
||||
"UnityEngine.PhysicsScene2D.CircleCast",
|
||||
"UnityEngine.PhysicsScene2D.GetRayIntersection",
|
||||
"UnityEngine.PhysicsScene2D.Linecast",
|
||||
"UnityEngine.PhysicsScene2D.OverlapArea",
|
||||
"UnityEngine.PhysicsScene2D.OverlapBox",
|
||||
"UnityEngine.PhysicsScene2D.OverlapCapsule",
|
||||
"UnityEngine.PhysicsScene2D.OverlapCircle",
|
||||
"UnityEngine.PhysicsScene2D.OverlapCollider",
|
||||
"UnityEngine.PhysicsScene2D.OverlapPoint",
|
||||
"UnityEngine.PhysicsScene2D.Raycast",
|
||||
"UnityEngine.Playables.Playable.Create",
|
||||
"UnityEngine.Profiling.CustomSampler.Create",
|
||||
"UnityEngine.RaycastCommand.ScheduleBatch",
|
||||
"UnityEngine.RemoteConfigSettings+QueueConfigDelegate.Invoke",
|
||||
"UnityEngine.RemoteConfigSettings.QueueConfig",
|
||||
"UnityEngine.RenderTexture.GetTemporaryImpl",
|
||||
"UnityEngine.Rendering.AsyncGPUReadback.Request",
|
||||
"UnityEngine.Rendering.AttachmentDescriptor.ConfigureClear",
|
||||
"UnityEngine.Rendering.BatchRendererGroup+AddBatch_InjectedDelegate.Invoke",
|
||||
"UnityEngine.Rendering.BatchRendererGroup.AddBatch",
|
||||
"UnityEngine.Rendering.BatchRendererGroup.AddBatch_Injected",
|
||||
"UnityEngine.Rendering.CommandBuffer+Internal_DispatchRaysDelegate.Invoke",
|
||||
"UnityEngine.Rendering.CommandBuffer.DispatchRays",
|
||||
"UnityEngine.Rendering.CommandBuffer.DrawMeshInstancedProcedural",
|
||||
"UnityEngine.Rendering.CommandBuffer.Internal_DispatchRays",
|
||||
"UnityEngine.Rendering.CommandBuffer.ResolveAntiAliasedSurface",
|
||||
"UnityEngine.Rendering.ScriptableRenderContext.BeginRenderPass",
|
||||
"UnityEngine.Rendering.ScriptableRenderContext.BeginScopedRenderPass",
|
||||
"UnityEngine.Rendering.ScriptableRenderContext.BeginScopedSubPass",
|
||||
"UnityEngine.Rendering.ScriptableRenderContext.BeginSubPass",
|
||||
"UnityEngine.Rendering.ScriptableRenderContext.SetupCameraProperties",
|
||||
"UnityEngine.Rigidbody2D.Cast",
|
||||
"UnityEngine.Scripting.GarbageCollector+CollectIncrementalDelegate.Invoke",
|
||||
"UnityEngine.Scripting.GarbageCollector.CollectIncremental",
|
||||
"UnityEngine.SpherecastCommand.ScheduleBatch",
|
||||
"UnityEngine.Texture.GetPixelDataSize",
|
||||
"UnityEngine.Texture.GetPixelDataOffset",
|
||||
"UnityEngine.Texture.GetPixelDataOffset",
|
||||
"UnityEngine.Texture2D+SetPixelDataImplArrayDelegate.Invoke",
|
||||
"UnityEngine.Texture2D+SetPixelDataImplDelegate.Invoke",
|
||||
"UnityEngine.Texture2D.SetPixelDataImpl",
|
||||
"UnityEngine.Texture2D.SetPixelDataImplArray",
|
||||
"UnityEngine.Texture2DArray+SetPixelDataImplArrayDelegate.Invoke",
|
||||
"UnityEngine.Texture2DArray+SetPixelDataImplDelegate.Invoke",
|
||||
"UnityEngine.Texture2DArray.SetPixelDataImpl",
|
||||
"UnityEngine.Texture2DArray.SetPixelDataImplArray",
|
||||
"UnityEngine.Texture3D+SetPixelDataImplArrayDelegate.Invoke",
|
||||
"UnityEngine.Texture3D+SetPixelDataImplDelegate.Invoke",
|
||||
"UnityEngine.Texture3D.SetPixelDataImpl",
|
||||
"UnityEngine.Texture3D.SetPixelDataImplArray",
|
||||
"UnityEngine.TrailRenderer+BakeMeshDelegate.Invoke",
|
||||
"UnityEngine.TrailRenderer.BakeMesh",
|
||||
"UnityEngine.WWW.LoadFromCacheOrDownload",
|
||||
"UnityEngine.XR.InputDevice.SendHapticImpulse",
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region IL2CPP IEnumerable and IDictionary
|
||||
|
||||
protected override bool Internal_TryGetEntryType(Type enumerableType, out Type type)
|
||||
{
|
||||
// Check for system types (not unhollowed)
|
||||
if (base.Internal_TryGetEntryType(enumerableType, out type))
|
||||
return true;
|
||||
|
||||
// Type is either an IL2CPP enumerable, or its not generic.
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
// Temporary naive solution until IL2CPP interface support improves.
|
||||
// This will work fine for most cases, but there are edge cases which would not work.
|
||||
type = type.GetGenericArguments()[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unable to determine entry type
|
||||
type = typeof(object);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool Internal_TryGetEntryTypes(Type type, out Type keys, out Type values)
|
||||
{
|
||||
if (base.Internal_TryGetEntryTypes(type, out keys, out values))
|
||||
return true;
|
||||
|
||||
// Type is either an IL2CPP dictionary, or its not generic.
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
// Naive solution until IL2CPP interfaces improve.
|
||||
var args = type.GetGenericArguments();
|
||||
if (args.Length == 2)
|
||||
{
|
||||
keys = args[0];
|
||||
values = args[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
keys = typeof(object);
|
||||
values = typeof(object);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Temp fix until Unhollower interface support improves
|
||||
|
||||
internal static readonly Dictionary<string, MethodInfo> getEnumeratorMethods = new Dictionary<string, MethodInfo>();
|
||||
internal static readonly Dictionary<string, EnumeratorInfo> enumeratorInfos = new Dictionary<string, EnumeratorInfo>();
|
||||
internal static readonly HashSet<string> notSupportedTypes = new HashSet<string>();
|
||||
|
||||
// IEnumerables
|
||||
|
||||
internal static IntPtr cppIEnumerablePointer;
|
||||
|
||||
protected override bool Internal_IsEnumerable(Type type)
|
||||
{
|
||||
if (base.Internal_IsEnumerable(type))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
if (cppIEnumerablePointer == IntPtr.Zero)
|
||||
Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IEnumerable), out cppIEnumerablePointer);
|
||||
|
||||
if (cppIEnumerablePointer != IntPtr.Zero
|
||||
&& Il2CppTypeNotNull(type, out IntPtr assignFromPtr)
|
||||
&& il2cpp_class_is_assignable_from(cppIEnumerablePointer, assignFromPtr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal class EnumeratorInfo
|
||||
{
|
||||
internal MethodInfo moveNext;
|
||||
internal PropertyInfo current;
|
||||
}
|
||||
|
||||
protected override bool Internal_TryGetEnumerator(object list, out IEnumerator enumerator)
|
||||
{
|
||||
if (list is IEnumerable)
|
||||
return base.Internal_TryGetEnumerator(list, out enumerator);
|
||||
|
||||
try
|
||||
{
|
||||
PrepareCppEnumerator(list, out object cppEnumerator, out EnumeratorInfo info);
|
||||
enumerator = EnumerateCppList(info, cppEnumerator);
|
||||
return true;
|
||||
}
|
||||
catch //(Exception ex)
|
||||
{
|
||||
//ExplorerCore.LogWarning($"Exception enumerating IEnumerable: {ex.ReflectionExToString()}");
|
||||
enumerator = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrepareCppEnumerator(object list, out object cppEnumerator, out EnumeratorInfo info)
|
||||
{
|
||||
info = null;
|
||||
cppEnumerator = null;
|
||||
if (list == null)
|
||||
throw new ArgumentNullException("list");
|
||||
|
||||
// Some ugly reflection to use the il2cpp interface for the instance type
|
||||
|
||||
var type = list.GetActualType();
|
||||
var key = type.AssemblyQualifiedName;
|
||||
|
||||
if (!getEnumeratorMethods.ContainsKey(key))
|
||||
{
|
||||
var method = type.GetMethod("GetEnumerator")
|
||||
?? type.GetMethod("System_Collections_IEnumerable_GetEnumerator", FLAGS);
|
||||
getEnumeratorMethods.Add(key, method);
|
||||
|
||||
// ensure the enumerator type is supported
|
||||
try
|
||||
{
|
||||
var test = getEnumeratorMethods[key].Invoke(list, null);
|
||||
test.GetActualType().GetMethod("MoveNext").Invoke(test, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log($"IEnumerable failed to enumerate: {ex}");
|
||||
notSupportedTypes.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (notSupportedTypes.Contains(key))
|
||||
throw new NotSupportedException($"The IEnumerable type '{type.FullName}' does not support MoveNext.");
|
||||
|
||||
cppEnumerator = getEnumeratorMethods[key].Invoke(list, null);
|
||||
var enumeratorType = cppEnumerator.GetActualType();
|
||||
|
||||
var enumInfoKey = enumeratorType.AssemblyQualifiedName;
|
||||
|
||||
if (!enumeratorInfos.ContainsKey(enumInfoKey))
|
||||
{
|
||||
enumeratorInfos.Add(enumInfoKey, new EnumeratorInfo
|
||||
{
|
||||
current = enumeratorType.GetProperty("Current"),
|
||||
moveNext = enumeratorType.GetMethod("MoveNext"),
|
||||
});
|
||||
}
|
||||
|
||||
info = enumeratorInfos[enumInfoKey];
|
||||
}
|
||||
|
||||
internal static IEnumerator EnumerateCppList(EnumeratorInfo info, object enumerator)
|
||||
{
|
||||
// Yield and return the actual entries
|
||||
while ((bool)info.moveNext.Invoke(enumerator, null))
|
||||
yield return info.current.GetValue(enumerator);
|
||||
}
|
||||
|
||||
// IDictionary
|
||||
|
||||
internal static IntPtr cppIDictionaryPointer;
|
||||
|
||||
protected override bool Internal_IsDictionary(Type type)
|
||||
{
|
||||
if (base.Internal_IsDictionary(type))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
if (cppIDictionaryPointer == IntPtr.Zero)
|
||||
if (!Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IDictionary), out cppIDictionaryPointer))
|
||||
return false;
|
||||
|
||||
if (Il2CppTypeNotNull(type, out IntPtr classPtr)
|
||||
&& il2cpp_class_is_assignable_from(cppIDictionaryPointer, classPtr))
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool Internal_TryGetDictEnumerator(object dictionary, out IEnumerator<DictionaryEntry> dictEnumerator)
|
||||
{
|
||||
if (dictionary is IDictionary)
|
||||
return base.Internal_TryGetDictEnumerator(dictionary, out dictEnumerator);
|
||||
|
||||
try
|
||||
{
|
||||
var type = dictionary.GetActualType();
|
||||
|
||||
if (typeof(Il2CppSystem.Collections.Hashtable).IsAssignableFrom(type))
|
||||
{
|
||||
dictEnumerator = EnumerateCppHashTable(dictionary.TryCast<Il2CppSystem.Collections.Hashtable>());
|
||||
return true;
|
||||
}
|
||||
|
||||
var keys = type.GetProperty("Keys").GetValue(dictionary, null);
|
||||
|
||||
var keyCollType = keys.GetActualType();
|
||||
var cacheKey = keyCollType.AssemblyQualifiedName;
|
||||
if (!getEnumeratorMethods.ContainsKey(cacheKey))
|
||||
{
|
||||
var method = keyCollType.GetMethod("GetEnumerator")
|
||||
?? keyCollType.GetMethod("System_Collections_IDictionary_GetEnumerator", FLAGS);
|
||||
getEnumeratorMethods.Add(cacheKey, method);
|
||||
|
||||
// test support
|
||||
try
|
||||
{
|
||||
var test = getEnumeratorMethods[cacheKey].Invoke(keys, null);
|
||||
test.GetActualType().GetMethod("MoveNext").Invoke(test, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log($"IDictionary failed to enumerate: {ex}");
|
||||
notSupportedTypes.Add(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (notSupportedTypes.Contains(cacheKey))
|
||||
throw new Exception($"The IDictionary type '{type.FullName}' does not support MoveNext.");
|
||||
|
||||
var keyEnumerator = getEnumeratorMethods[cacheKey].Invoke(keys, null);
|
||||
var keyInfo = new EnumeratorInfo
|
||||
{
|
||||
current = keyEnumerator.GetActualType().GetProperty("Current"),
|
||||
moveNext = keyEnumerator.GetActualType().GetMethod("MoveNext"),
|
||||
};
|
||||
|
||||
var values = type.GetProperty("Values").GetValue(dictionary, null);
|
||||
var valueEnumerator = values.GetActualType().GetMethod("GetEnumerator").Invoke(values, null);
|
||||
var valueInfo = new EnumeratorInfo
|
||||
{
|
||||
current = valueEnumerator.GetActualType().GetProperty("Current"),
|
||||
moveNext = valueEnumerator.GetActualType().GetMethod("MoveNext"),
|
||||
};
|
||||
|
||||
dictEnumerator = EnumerateCppDict(keyInfo, keyEnumerator, valueInfo, valueEnumerator);
|
||||
return true;
|
||||
}
|
||||
catch //(Exception ex)
|
||||
{
|
||||
//ExplorerCore.LogWarning($"Exception enumerating IDictionary: {ex.ReflectionExToString()}");
|
||||
dictEnumerator = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerator<DictionaryEntry> EnumerateCppDict(EnumeratorInfo keyInfo, object keyEnumerator,
|
||||
EnumeratorInfo valueInfo, object valueEnumerator)
|
||||
{
|
||||
while ((bool)keyInfo.moveNext.Invoke(keyEnumerator, null))
|
||||
{
|
||||
valueInfo.moveNext.Invoke(valueEnumerator, null);
|
||||
|
||||
var key = keyInfo.current.GetValue(keyEnumerator, null);
|
||||
var value = valueInfo.current.GetValue(valueEnumerator, null);
|
||||
|
||||
yield return new DictionaryEntry(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerator<DictionaryEntry> EnumerateCppHashTable(Il2CppSystem.Collections.Hashtable hashtable)
|
||||
{
|
||||
for (int i = 0; i < hashtable.buckets.Count; i++)
|
||||
{
|
||||
var bucket = hashtable.buckets[i];
|
||||
if (bucket == null || bucket.key == null)
|
||||
continue;
|
||||
|
||||
yield return new DictionaryEntry(bucket.key, bucket.val);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
57
src/Core/Reflection/Patches.cs
Normal file
57
src/Core/Reflection/Patches.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
public static class ReflectionPatches
|
||||
{
|
||||
public static void Init()
|
||||
{
|
||||
try
|
||||
{
|
||||
var method = typeof(Assembly).GetMethod(nameof(Assembly.GetTypes), new Type[0]);
|
||||
var processor = ExplorerCore.Harmony.CreateProcessor(method);
|
||||
processor.AddFinalizer(typeof(ReflectionPatches).GetMethod(nameof(Assembly_GetTypes)));
|
||||
processor.Patch();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting up Reflection patch: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Type[] emptyTypes = new Type[0];
|
||||
|
||||
public static Exception Assembly_GetTypes(Assembly __instance, Exception __exception, ref Type[] __result)
|
||||
{
|
||||
if (__exception != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
__result = __instance.GetExportedTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
try
|
||||
{
|
||||
__result = e.Types.Where(it => it != null).ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
__result = emptyTypes;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
__result = emptyTypes;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
597
src/Core/Reflection/ReflectionUtility.cs
Normal file
597
src/Core/Reflection/ReflectionUtility.cs
Normal file
@ -0,0 +1,597 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Core.Config;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
using BF = System.Reflection.BindingFlags;
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
public class ReflectionUtility
|
||||
{
|
||||
public const BF FLAGS = BF.Public | BF.Instance | BF.NonPublic | BF.Static;
|
||||
|
||||
internal static ReflectionUtility Instance;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
Instance =
|
||||
#if CPP
|
||||
new Il2CppReflection();
|
||||
#else
|
||||
new ReflectionUtility();
|
||||
#endif
|
||||
Instance.Initialize();
|
||||
|
||||
ReflectionPatches.Init();
|
||||
}
|
||||
|
||||
protected virtual void Initialize()
|
||||
{
|
||||
SetupTypeCache();
|
||||
|
||||
LoadBlacklistString(ConfigManager.Reflection_Signature_Blacklist.Value);
|
||||
ConfigManager.Reflection_Signature_Blacklist.OnValueChanged += LoadBlacklistString;
|
||||
}
|
||||
|
||||
#region Type cache
|
||||
|
||||
public static Action<Type> OnTypeLoaded;
|
||||
|
||||
/// <summary>Key: Type.FullName</summary>
|
||||
protected static readonly SortedDictionary<string, Type> AllTypes = new SortedDictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static readonly List<string> AllNamespaces = new List<string>();
|
||||
private static readonly HashSet<string> uniqueNamespaces = new HashSet<string>();
|
||||
|
||||
private static string[] allTypesArray;
|
||||
public static string[] GetTypeNameArray()
|
||||
{
|
||||
if (allTypesArray == null || allTypesArray.Length != AllTypes.Count)
|
||||
{
|
||||
allTypesArray = new string[AllTypes.Count];
|
||||
int i = 0;
|
||||
foreach (var name in AllTypes.Keys)
|
||||
{
|
||||
allTypesArray[i] = name;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return allTypesArray;
|
||||
}
|
||||
|
||||
private static void SetupTypeCache()
|
||||
{
|
||||
float start = Time.realtimeSinceStartup;
|
||||
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
CacheTypes(asm);
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyLoad += AssemblyLoaded;
|
||||
|
||||
ExplorerCore.Log($"Cached AppDomain assemblies in {Time.realtimeSinceStartup - start} seconds");
|
||||
}
|
||||
|
||||
private static void AssemblyLoaded(object sender, AssemblyLoadEventArgs args)
|
||||
{
|
||||
if (args.LoadedAssembly == null || args.LoadedAssembly.GetName().Name == "completions")
|
||||
return;
|
||||
|
||||
CacheTypes(args.LoadedAssembly);
|
||||
}
|
||||
|
||||
private static void CacheTypes(Assembly asm)
|
||||
{
|
||||
foreach (var type in asm.TryGetTypes())
|
||||
{
|
||||
// Cache namespace if there is one
|
||||
if (!string.IsNullOrEmpty(type.Namespace) && !uniqueNamespaces.Contains(type.Namespace))
|
||||
{
|
||||
uniqueNamespaces.Add(type.Namespace);
|
||||
int i = 0;
|
||||
while (i < AllNamespaces.Count)
|
||||
{
|
||||
if (type.Namespace.CompareTo(AllNamespaces[i]) < 0)
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
AllNamespaces.Insert(i, type.Namespace);
|
||||
}
|
||||
|
||||
// Cache the type. Overwrite type if one exists with the full name
|
||||
if (AllTypes.ContainsKey(type.FullName))
|
||||
AllTypes[type.FullName] = type;
|
||||
else
|
||||
AllTypes.Add(type.FullName, type);
|
||||
|
||||
// Invoke listener
|
||||
OnTypeLoaded?.Invoke(type);
|
||||
|
||||
// Check type inheritance cache, add this to any lists it should be in
|
||||
foreach (var key in typeInheritance.Keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseType = AllTypes[key];
|
||||
if (baseType.IsAssignableFrom(type) && !typeInheritance[key].Contains(type))
|
||||
typeInheritance[key].Add(type);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Find a <see cref="Type"/> in the current AppDomain whose <see cref="Type.FullName"/> matches the provided <paramref name="fullName"/>.
|
||||
/// </summary>
|
||||
/// <param name="fullName">The <see cref="Type.FullName"/> you want to search for - case sensitive and full matches only.</param>
|
||||
/// <returns>The Type if found, otherwise null.</returns>
|
||||
public static Type GetTypeByName(string fullName)
|
||||
=> Instance.Internal_GetTypeByName(fullName);
|
||||
|
||||
internal virtual Type Internal_GetTypeByName(string fullName)
|
||||
{
|
||||
AllTypes.TryGetValue(fullName, out Type type);
|
||||
return type;
|
||||
}
|
||||
|
||||
// Getting the actual type of an object
|
||||
internal virtual Type Internal_GetActualType(object obj)
|
||||
=> obj?.GetType();
|
||||
|
||||
// Force-casting an object to a type
|
||||
internal virtual object Internal_TryCast(object obj, Type castTo)
|
||||
=> obj;
|
||||
|
||||
// Processing deobfuscated type names in strings
|
||||
public static string ProcessTypeInString(Type type, string theString)
|
||||
=> Instance.Internal_ProcessTypeInString(theString, type);
|
||||
|
||||
internal virtual string Internal_ProcessTypeInString(string theString, Type type)
|
||||
=> theString;
|
||||
|
||||
// Singleton finder
|
||||
|
||||
public static void FindSingleton(string[] possibleNames, Type type, BindingFlags flags, List<object> instances)
|
||||
=> Instance.Internal_FindSingleton(possibleNames, type, flags, instances);
|
||||
|
||||
internal virtual void Internal_FindSingleton(string[] possibleNames, Type type, BindingFlags flags, List<object> instances)
|
||||
{
|
||||
// Look for a typical Instance backing field.
|
||||
FieldInfo fi;
|
||||
foreach (var name in possibleNames)
|
||||
{
|
||||
fi = type.GetField(name, flags);
|
||||
if (fi != null)
|
||||
{
|
||||
var instance = fi.GetValue(null);
|
||||
if (instance != null)
|
||||
{
|
||||
instances.Add(instance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Universal helpers
|
||||
|
||||
#region Type inheritance cache
|
||||
|
||||
// cache for GetBaseTypes
|
||||
internal static readonly Dictionary<string, Type[]> baseTypes = new Dictionary<string, Type[]>();
|
||||
|
||||
/// <summary>
|
||||
/// Get all base types of the provided Type, including itself.
|
||||
/// </summary>
|
||||
public static Type[] GetAllBaseTypes(object obj) => GetAllBaseTypes(obj?.GetActualType());
|
||||
|
||||
/// <summary>
|
||||
/// Get all base types of the provided Type, including itself.
|
||||
/// </summary>
|
||||
public static Type[] GetAllBaseTypes(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException("type");
|
||||
|
||||
var name = type.AssemblyQualifiedName;
|
||||
|
||||
if (baseTypes.TryGetValue(name, out Type[] ret))
|
||||
return ret;
|
||||
|
||||
List<Type> list = new List<Type>();
|
||||
|
||||
while (type != null)
|
||||
{
|
||||
list.Add(type);
|
||||
type = type.BaseType;
|
||||
}
|
||||
|
||||
ret = list.ToArray();
|
||||
|
||||
baseTypes.Add(name, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Type and Generic Parameter implementation cache
|
||||
|
||||
// cache for GetImplementationsOf
|
||||
internal static readonly Dictionary<string, HashSet<Type>> typeInheritance = new Dictionary<string, HashSet<Type>>();
|
||||
internal static readonly Dictionary<string, HashSet<Type>> genericParameterInheritance = new Dictionary<string, HashSet<Type>>();
|
||||
|
||||
public static string GetImplementationKey(Type type)
|
||||
{
|
||||
if (!type.IsGenericParameter)
|
||||
return type.FullName;
|
||||
else
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(type.GenericParameterAttributes)
|
||||
.Append('|');
|
||||
foreach (var c in type.GetGenericParameterConstraints())
|
||||
sb.Append(c.FullName).Append(',');
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all non-abstract implementations of the provided type (include itself, if not abstract) in the current AppDomain.
|
||||
/// Also works for generic parameters by analyzing the constraints.
|
||||
/// </summary>
|
||||
/// <param name="baseType">The base type, which can optionally be abstract / interface.</param>
|
||||
/// <returns>All implementations of the type in the current AppDomain.</returns>
|
||||
public static HashSet<Type> GetImplementationsOf(Type baseType, bool allowAbstract, bool allowGeneric, bool allowEnum, bool allowRecursive = true)
|
||||
{
|
||||
var key = GetImplementationKey(baseType);
|
||||
|
||||
int count = AllTypes.Count;
|
||||
HashSet<Type> ret;
|
||||
if (!baseType.IsGenericParameter)
|
||||
ret = GetImplementations(key, baseType, allowAbstract, allowGeneric, allowEnum);
|
||||
else
|
||||
ret = GetGenericParameterImplementations(key, baseType, allowAbstract, allowGeneric);
|
||||
|
||||
// types were resolved during the parse, do it again if we're not already rebuilding.
|
||||
if (allowRecursive && AllTypes.Count != count)
|
||||
ret = GetImplementationsOf(baseType, allowAbstract, allowGeneric, false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static HashSet<Type> GetImplementations(string key, Type baseType, bool allowAbstract, bool allowGeneric, bool allowEnum)
|
||||
{
|
||||
if (!typeInheritance.ContainsKey(key))
|
||||
{
|
||||
var set = new HashSet<Type>();
|
||||
var names = GetTypeNameArray();
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
var name = names[i];
|
||||
try
|
||||
{
|
||||
var type = AllTypes[name];
|
||||
|
||||
if (set.Contains(type)
|
||||
|| (type.IsAbstract && type.IsSealed) // ignore static classes
|
||||
|| (!allowAbstract && type.IsAbstract)
|
||||
|| (!allowGeneric && (type.IsGenericType || type.IsGenericTypeDefinition))
|
||||
|| (!allowEnum && type.IsEnum))
|
||||
continue;
|
||||
|
||||
if (type.FullName.Contains("PrivateImplementationDetails")
|
||||
|| type.FullName.Contains("DisplayClass")
|
||||
|| type.FullName.Contains('<'))
|
||||
continue;
|
||||
|
||||
if (baseType.IsAssignableFrom(type) && !set.Contains(type))
|
||||
set.Add(type);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
//set.
|
||||
|
||||
typeInheritance.Add(key, set);
|
||||
}
|
||||
|
||||
return typeInheritance[key];
|
||||
}
|
||||
|
||||
private static HashSet<Type> GetGenericParameterImplementations(string key, Type baseType, bool allowAbstract, bool allowGeneric)
|
||||
{
|
||||
if (!genericParameterInheritance.ContainsKey(key))
|
||||
{
|
||||
var set = new HashSet<Type>();
|
||||
|
||||
var names = GetTypeNameArray();
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
var name = names[i];
|
||||
try
|
||||
{
|
||||
var type = AllTypes[name];
|
||||
|
||||
if (set.Contains(type)
|
||||
|| (type.IsAbstract && type.IsSealed) // ignore static classes
|
||||
|| (!allowAbstract && type.IsAbstract)
|
||||
|| (!allowGeneric && (type.IsGenericType || type.IsGenericTypeDefinition)))
|
||||
continue;
|
||||
|
||||
if (type.FullName.Contains("PrivateImplementationDetails")
|
||||
|| type.FullName.Contains("DisplayClass")
|
||||
|| type.FullName.Contains('<'))
|
||||
continue;
|
||||
|
||||
if (baseType.GenericParameterAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)
|
||||
&& type.IsClass)
|
||||
continue;
|
||||
|
||||
if (baseType.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)
|
||||
&& type.IsValueType)
|
||||
continue;
|
||||
|
||||
if (baseType.GetGenericParameterConstraints().Any(it => !it.IsAssignableFrom(type)))
|
||||
continue;
|
||||
|
||||
set.Add(type);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
genericParameterInheritance.Add(key, set);
|
||||
}
|
||||
|
||||
return genericParameterInheritance[key];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Internal MemberInfo Cache
|
||||
|
||||
internal static Dictionary<Type, Dictionary<string, FieldInfo>> fieldInfos = new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
|
||||
public static FieldInfo GetFieldInfo(Type type, string fieldName)
|
||||
{
|
||||
if (!fieldInfos.ContainsKey(type))
|
||||
fieldInfos.Add(type, new Dictionary<string, FieldInfo>());
|
||||
|
||||
if (!fieldInfos[type].ContainsKey(fieldName))
|
||||
fieldInfos[type].Add(fieldName, type.GetField(fieldName, FLAGS));
|
||||
|
||||
return fieldInfos[type][fieldName];
|
||||
}
|
||||
|
||||
internal static Dictionary<Type, Dictionary<string, PropertyInfo>> propertyInfos = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
|
||||
public static PropertyInfo GetPropertyInfo(Type type, string propertyName)
|
||||
{
|
||||
if (!propertyInfos.ContainsKey(type))
|
||||
propertyInfos.Add(type, new Dictionary<string, PropertyInfo>());
|
||||
|
||||
if (!propertyInfos[type].ContainsKey(propertyName))
|
||||
propertyInfos[type].Add(propertyName, type.GetProperty(propertyName, FLAGS));
|
||||
|
||||
return propertyInfos[type][propertyName];
|
||||
}
|
||||
|
||||
internal static Dictionary<Type, Dictionary<string, MethodInfo>> methodInfos = new Dictionary<Type, Dictionary<string, MethodInfo>>();
|
||||
|
||||
public static MethodInfo GetMethodInfo(Type type, string methodName)
|
||||
=> GetMethodInfo(type, methodName, ArgumentUtility.EmptyTypes, false);
|
||||
|
||||
public static MethodInfo GetMethodInfo(Type type, string methodName, Type[] argumentTypes, bool cacheAmbiguous = false)
|
||||
{
|
||||
if (!methodInfos.ContainsKey(type))
|
||||
methodInfos.Add(type, new Dictionary<string, MethodInfo>());
|
||||
|
||||
if (cacheAmbiguous)
|
||||
{
|
||||
methodName += "|";
|
||||
foreach (var arg in argumentTypes)
|
||||
methodName += arg.FullName + ",";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!methodInfos[type].ContainsKey(methodName))
|
||||
{
|
||||
if (argumentTypes != null)
|
||||
methodInfos[type].Add(methodName, type.GetMethod(methodName, FLAGS, null, argumentTypes, null));
|
||||
else
|
||||
methodInfos[type].Add(methodName, type.GetMethod(methodName, FLAGS));
|
||||
}
|
||||
|
||||
return methodInfos[type][methodName];
|
||||
}
|
||||
catch (AmbiguousMatchException)
|
||||
{
|
||||
ExplorerCore.LogWarning($"AmbiguousMatchException trying to get method '{methodName}'");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ExplorerCore.LogWarning($"{e.GetType()} trying to get method '{methodName}': {e.Message}\r\n{e.StackTrace}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Reflection Blacklist
|
||||
|
||||
public virtual string[] DefaultReflectionBlacklist => new string[0];
|
||||
|
||||
public static void LoadBlacklistString(string blacklist)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(blacklist) && !Instance.DefaultReflectionBlacklist.Any())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var sigs = blacklist.Split(';');
|
||||
foreach (var sig in sigs)
|
||||
{
|
||||
var s = sig.Trim();
|
||||
if (string.IsNullOrEmpty(s))
|
||||
continue;
|
||||
if (!currentBlacklist.Contains(s))
|
||||
currentBlacklist.Add(s);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception parsing blacklist string: {ex.ReflectionExToString()}");
|
||||
}
|
||||
|
||||
foreach (var sig in Instance.DefaultReflectionBlacklist)
|
||||
{
|
||||
if (!currentBlacklist.Contains(sig))
|
||||
currentBlacklist.Add(sig);
|
||||
}
|
||||
|
||||
Mono.CSharp.IL2CPP.Blacklist.SignatureBlacklist = currentBlacklist;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Exception setting up reflection blacklist: {ex.ReflectionExToString()}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsBlacklisted(MemberInfo member)
|
||||
{
|
||||
if (string.IsNullOrEmpty(member.DeclaringType?.Namespace))
|
||||
return false;
|
||||
|
||||
var sig = $"{member.DeclaringType.FullName}.{member.Name}";
|
||||
|
||||
return currentBlacklist.Contains(sig);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> currentBlacklist = new HashSet<string>();
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
// Temp fix for IL2CPP until interface support improves
|
||||
|
||||
// IsEnumerable
|
||||
|
||||
public static bool IsEnumerable(Type type) => Instance.Internal_IsEnumerable(type);
|
||||
|
||||
protected virtual bool Internal_IsEnumerable(Type type)
|
||||
{
|
||||
return typeof(IEnumerable).IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
// TryGetEnumerator (list)
|
||||
|
||||
public static bool TryGetEnumerator(object list, out IEnumerator enumerator)
|
||||
=> Instance.Internal_TryGetEnumerator(list, out enumerator);
|
||||
|
||||
protected virtual bool Internal_TryGetEnumerator(object list, out IEnumerator enumerator)
|
||||
{
|
||||
enumerator = (list as IEnumerable).GetEnumerator();
|
||||
return true;
|
||||
}
|
||||
|
||||
// TryGetEntryType
|
||||
|
||||
public static bool TryGetEntryType(Type enumerableType, out Type type)
|
||||
=> Instance.Internal_TryGetEntryType(enumerableType, out type);
|
||||
|
||||
protected virtual bool Internal_TryGetEntryType(Type enumerableType, out Type type)
|
||||
{
|
||||
// Check for arrays
|
||||
if (enumerableType.IsArray)
|
||||
{
|
||||
type = enumerableType.GetElementType();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for implementation of IEnumerable<T>, IList<T> or ICollection<T>
|
||||
foreach (var t in enumerableType.GetInterfaces())
|
||||
{
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
var typeDef = t.GetGenericTypeDefinition();
|
||||
if (typeDef == typeof(IEnumerable<>) || typeDef == typeof(IList<>) || typeDef == typeof(ICollection<>))
|
||||
{
|
||||
type = t.GetGenericArguments()[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unable to determine any generic element type, just use object.
|
||||
type = typeof(object);
|
||||
return false;
|
||||
}
|
||||
|
||||
// IsDictionary
|
||||
|
||||
public static bool IsDictionary(Type type) => Instance.Internal_IsDictionary(type);
|
||||
|
||||
protected virtual bool Internal_IsDictionary(Type type)
|
||||
{
|
||||
return typeof(IDictionary).IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
// TryGetEnumerator (dictionary)
|
||||
|
||||
public static bool TryGetDictEnumerator(object dictionary, out IEnumerator<DictionaryEntry> dictEnumerator)
|
||||
=> Instance.Internal_TryGetDictEnumerator(dictionary, out dictEnumerator);
|
||||
|
||||
protected virtual bool Internal_TryGetDictEnumerator(object dictionary, out IEnumerator<DictionaryEntry> dictEnumerator)
|
||||
{
|
||||
dictEnumerator = EnumerateDictionary((IDictionary)dictionary);
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator<DictionaryEntry> EnumerateDictionary(IDictionary dict)
|
||||
{
|
||||
var enumerator = dict.GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return new DictionaryEntry(enumerator.Key, enumerator.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// TryGetEntryTypes
|
||||
|
||||
public static bool TryGetEntryTypes(Type dictionaryType, out Type keys, out Type values)
|
||||
=> Instance.Internal_TryGetEntryTypes(dictionaryType, out keys, out values);
|
||||
|
||||
protected virtual bool Internal_TryGetEntryTypes(Type dictionaryType, out Type keys, out Type values)
|
||||
{
|
||||
foreach (var t in dictionaryType.GetInterfaces())
|
||||
{
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>))
|
||||
{
|
||||
var args = t.GetGenericArguments();
|
||||
keys = args[0];
|
||||
values = args[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
keys = typeof(object);
|
||||
values = typeof(object);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BF = System.Reflection.BindingFlags;
|
||||
using UnityExplorer.Core.Runtime;
|
||||
|
||||
namespace UnityExplorer
|
||||
{
|
||||
public static class ReflectionUtility
|
||||
{
|
||||
public const BF AllFlags = BF.Public | BF.Instance | BF.NonPublic | BF.Static;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for IL2CPP to get the underlying true Type (Unhollowed) of the object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to get the true Type for.</param>
|
||||
/// <returns>The most accurate Type of the object which could be identified.</returns>
|
||||
public static Type GetActualType(this object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
return ReflectionProvider.Instance.GetActualType(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cast an object to its underlying Type.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to cast</param>
|
||||
/// <returns>The object, cast to the underlying Type if possible, otherwise the original object.</returns>
|
||||
public static object Cast(this object obj)
|
||||
=> ReflectionProvider.Instance.Cast(obj, GetActualType(obj));
|
||||
|
||||
/// <summary>
|
||||
/// Cast an object to a Type, if possible.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to cast</param>
|
||||
/// <param name="castTo">The Type to cast to </param>
|
||||
/// <returns>The object, cast to the Type provided if possible, otherwise the original object.</returns>
|
||||
public static object Cast(this object obj, Type castTo)
|
||||
=> ReflectionProvider.Instance.Cast(obj, castTo);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the provided Type is assignable to IEnumerable.
|
||||
/// </summary>
|
||||
/// <param name="t">The Type to check</param>
|
||||
/// <returns>True if the Type is assignable to IEnumerable, otherwise false.</returns>
|
||||
public static bool IsEnumerable(this Type t)
|
||||
=> ReflectionProvider.Instance.IsAssignableFrom(typeof(IEnumerable), t);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the provided Type is assignable to IDictionary.
|
||||
/// </summary>
|
||||
/// <param name="t">The Type to check</param>
|
||||
/// <returns>True if the Type is assignable to IDictionary, otherwise false.</returns>
|
||||
public static bool IsDictionary(this Type t)
|
||||
=> ReflectionProvider.Instance.IsAssignableFrom(typeof(IDictionary), t);
|
||||
|
||||
/// <summary>
|
||||
/// [INTERNAL] Used to load Unhollowed DLLs in IL2CPP.
|
||||
/// </summary>
|
||||
internal static bool LoadModule(string module)
|
||||
=> ReflectionProvider.Instance.LoadModule(module);
|
||||
|
||||
// cache for GetTypeByName
|
||||
internal static readonly Dictionary<string, Type> s_typesByName = new Dictionary<string, Type>();
|
||||
|
||||
/// <summary>
|
||||
/// Find a <see cref="Type"/> in the current AppDomain whose <see cref="Type.FullName"/> matches the provided <paramref name="fullName"/>.
|
||||
/// </summary>
|
||||
/// <param name="fullName">The <see cref="Type.FullName"/> you want to search for - case sensitive and full matches only.</param>
|
||||
/// <returns>The Type if found, otherwise null.</returns>
|
||||
public static Type GetTypeByName(string fullName)
|
||||
{
|
||||
s_typesByName.TryGetValue(fullName, out Type ret);
|
||||
|
||||
if (ret != null)
|
||||
return ret;
|
||||
|
||||
foreach (var type in from asm in AppDomain.CurrentDomain.GetAssemblies()
|
||||
from type in asm.TryGetTypes()
|
||||
select type)
|
||||
{
|
||||
if (type.FullName == fullName)
|
||||
{
|
||||
ret = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_typesByName.ContainsKey(fullName))
|
||||
s_typesByName[fullName] = ret;
|
||||
else
|
||||
s_typesByName.Add(fullName, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// cache for GetBaseTypes
|
||||
internal static readonly Dictionary<string, Type[]> s_cachedTypeInheritance = new Dictionary<string, Type[]>();
|
||||
|
||||
/// <summary>
|
||||
/// Get all base types of the provided Type, including itself.
|
||||
/// </summary>
|
||||
public static Type[] GetAllBaseTypes(this object obj) => GetAllBaseTypes(GetActualType(obj));
|
||||
|
||||
/// <summary>
|
||||
/// Get all base types of the provided Type, including itself.
|
||||
/// </summary>
|
||||
public static Type[] GetAllBaseTypes(this Type type)
|
||||
{
|
||||
if (type == null)
|
||||
throw new ArgumentNullException("type");
|
||||
|
||||
var name = type.AssemblyQualifiedName;
|
||||
|
||||
if (s_cachedTypeInheritance.TryGetValue(name, out Type[] ret))
|
||||
return ret;
|
||||
|
||||
List<Type> list = new List<Type>();
|
||||
|
||||
while (type != null)
|
||||
{
|
||||
list.Add(type);
|
||||
type = type.BaseType;
|
||||
}
|
||||
|
||||
ret = list.ToArray();
|
||||
|
||||
s_cachedTypeInheritance.Add(name, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely get all valid Types inside an Assembly.
|
||||
/// </summary>
|
||||
/// <param name="asm">The Assembly to find Types in.</param>
|
||||
/// <returns>All possible Types which could be retrieved from the Assembly, or an empty array.</returns>
|
||||
public static IEnumerable<Type> TryGetTypes(this Assembly asm)
|
||||
{
|
||||
try
|
||||
{
|
||||
return asm.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
try
|
||||
{
|
||||
return asm.GetExportedTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return e.Types.Where(t => t != null);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<Type>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static Dictionary<Type, Dictionary<string, FieldInfo>> s_cachedFieldInfos = new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
|
||||
public static FieldInfo GetFieldInfo(Type type, string fieldName)
|
||||
{
|
||||
if (!s_cachedFieldInfos.ContainsKey(type))
|
||||
s_cachedFieldInfos.Add(type, new Dictionary<string, FieldInfo>());
|
||||
|
||||
if (!s_cachedFieldInfos[type].ContainsKey(fieldName))
|
||||
s_cachedFieldInfos[type].Add(fieldName, type.GetField(fieldName, AllFlags));
|
||||
|
||||
return s_cachedFieldInfos[type][fieldName];
|
||||
}
|
||||
|
||||
internal static Dictionary<Type, Dictionary<string, PropertyInfo>> s_cachedPropInfos = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
|
||||
public static PropertyInfo GetPropertyInfo(Type type, string propertyName)
|
||||
{
|
||||
if (!s_cachedPropInfos.ContainsKey(type))
|
||||
s_cachedPropInfos.Add(type, new Dictionary<string, PropertyInfo>());
|
||||
|
||||
if (!s_cachedPropInfos[type].ContainsKey(propertyName))
|
||||
s_cachedPropInfos[type].Add(propertyName, type.GetProperty(propertyName, AllFlags));
|
||||
|
||||
return s_cachedPropInfos[type][propertyName];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to display a simple "{ExceptionType}: {Message}" of the exception, and optionally use the inner-most exception.
|
||||
/// </summary>
|
||||
/// <param name="e">The Exception to convert to string.</param>
|
||||
/// <param name="innerMost">Should the inner-most Exception of the stack be used? If false, the Exception you provided will be used directly.</param>
|
||||
/// <returns>The exception to string.</returns>
|
||||
public static string ReflectionExToString(this Exception e, bool innerMost = false)
|
||||
{
|
||||
if (innerMost)
|
||||
{
|
||||
while (e.InnerException != null)
|
||||
{
|
||||
#if CPP
|
||||
if (e.InnerException is System.Runtime.CompilerServices.RuntimeWrappedException)
|
||||
break;
|
||||
#endif
|
||||
e = e.InnerException;
|
||||
}
|
||||
}
|
||||
|
||||
return $"{e.GetType()}: {e.Message}";
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,7 @@ namespace UnityExplorer
|
||||
public static AssetBundle LoadFromFile(string path)
|
||||
{
|
||||
var iCall = ICallManager.GetICall<d_LoadFromFile>("UnityEngine.AssetBundle::LoadFromFile_Internal");
|
||||
|
||||
var ptr = iCall.Invoke(IL2CPP.ManagedStringToIl2Cpp(path), 0u, 0UL);
|
||||
|
||||
return new AssetBundle(ptr);
|
||||
}
|
||||
|
||||
@ -30,9 +28,7 @@ namespace UnityExplorer
|
||||
public static AssetBundle LoadFromMemory(byte[] binary, uint crc = 0)
|
||||
{
|
||||
var iCall = ICallManager.GetICall<d_LoadFromMemory>("UnityEngine.AssetBundle::LoadFromMemory_Internal");
|
||||
|
||||
var ptr = iCall(((Il2CppStructArray<byte>) binary).Pointer, crc);
|
||||
|
||||
return new AssetBundle(ptr);
|
||||
}
|
||||
|
||||
@ -71,6 +67,16 @@ namespace UnityExplorer
|
||||
|
||||
return new UnityEngine.Object(ptr).TryCast<T>();
|
||||
}
|
||||
|
||||
// Unload(bool unloadAllLoadedObjects);
|
||||
|
||||
internal delegate void d_Unload(IntPtr _this, bool unloadAllLoadedObjects);
|
||||
|
||||
public void Unload(bool unloadAssets = true)
|
||||
{
|
||||
var iCall = ICallManager.GetICall<d_Unload>("UnityEngine.AssetBundle::Unload");
|
||||
iCall.Invoke(this.m_bundlePtr, unloadAssets);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -7,7 +7,6 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
{
|
||||
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "External methods")]
|
||||
public static class ICallManager
|
||||
{
|
||||
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
|
@ -6,8 +6,8 @@ using System.Linq;
|
||||
using UnhollowerBaseLib;
|
||||
using UnityEngine;
|
||||
|
||||
// CREDIT HerpDerpenstine
|
||||
// https://github.com/LavaGang/MelonLoader/blob/master/MelonLoader.Support.Il2Cpp/MelonCoroutines.cs
|
||||
// Credit to HerpDerpenstine and knah
|
||||
// https://github.com/LavaGang/MelonLoader/blob/master/SM_Il2Cpp/Coroutines.cs
|
||||
|
||||
namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
{
|
||||
@ -124,8 +124,11 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
if (nextAsEnumerator != null) // il2cpp IEnumerator also handles CustomYieldInstruction
|
||||
next = new Il2CppEnumeratorWrapper(nextAsEnumerator);
|
||||
else
|
||||
ExplorerCore.LogWarning($"Unknown coroutine yield object of type {il2CppObjectBase} for coroutine {enumerator}");
|
||||
break;
|
||||
ExplorerCore.LogWarning($"Unknown coroutine yield object of type '{il2CppObjectBase}' for coroutine '{enumerator}'");
|
||||
return;
|
||||
default:
|
||||
ExplorerCore.LogWarning($"Unknown coroutine yield object of type '{next}' for coroutine '{enumerator}'");
|
||||
return;
|
||||
}
|
||||
|
||||
ourCoroutinesStore.Add(new CoroTuple { WaitCondition = next, Coroutine = enumerator });
|
||||
|
@ -21,7 +21,7 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
Reflection = new Il2CppReflection();
|
||||
ExplorerCore.Context = RuntimeContext.IL2CPP;
|
||||
TextureUtil = new Il2CppTextureUtil();
|
||||
}
|
||||
|
||||
@ -29,33 +29,60 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
{
|
||||
try
|
||||
{
|
||||
//Application.add_logMessageReceived(new Action<string, string, LogType>(ExplorerCore.Instance.OnUnityLog));
|
||||
|
||||
var logType = ReflectionUtility.GetTypeByName("UnityEngine.Application+LogCallback");
|
||||
var castMethod = logType.GetMethod("op_Implicit", new[] { typeof(Action<string, string, LogType>) });
|
||||
var addMethod = typeof(Application).GetMethod("add_logMessageReceived", BF.Static | BF.Public, null, new[] { logType }, null);
|
||||
addMethod.Invoke(null, new[]
|
||||
{
|
||||
castMethod.Invoke(null, new[] { new Action<string, string, LogType>(Application_logMessageReceived) })
|
||||
});
|
||||
Application.add_logMessageReceived(new Action<string, string, LogType>(Application_logMessageReceived));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning("Exception setting up Unity log listener, make sure Unity libraries have been unstripped!");
|
||||
ExplorerCore.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Application_logMessageReceived(string condition, string stackTrace, LogType type)
|
||||
{
|
||||
ExplorerCore.Log(condition, type, true);
|
||||
ExplorerCore.LogUnity(condition, type);
|
||||
}
|
||||
|
||||
public override void StartConsoleCoroutine(IEnumerator routine)
|
||||
public override void Update()
|
||||
{
|
||||
Il2CppCoroutine.Process();
|
||||
}
|
||||
|
||||
internal override void ProcessOnPostRender()
|
||||
{
|
||||
Il2CppCoroutine.ProcessWaitForEndOfFrame();
|
||||
}
|
||||
|
||||
internal override void ProcessFixedUpdate()
|
||||
{
|
||||
Il2CppCoroutine.ProcessWaitForFixedUpdate();
|
||||
}
|
||||
|
||||
public override void StartCoroutine(IEnumerator routine)
|
||||
{
|
||||
Il2CppCoroutine.Start(routine);
|
||||
}
|
||||
|
||||
// Unity API Handlers
|
||||
public override T AddComponent<T>(GameObject obj, Type type)
|
||||
{
|
||||
return obj.AddComponent(Il2CppType.From(type)).TryCast<T>();
|
||||
}
|
||||
|
||||
public override ScriptableObject CreateScriptable(Type type)
|
||||
{
|
||||
return ScriptableObject.CreateInstance(Il2CppType.From(type));
|
||||
}
|
||||
|
||||
// Pretty disgusting but couldn't figure out a cleaner way yet unfortunately
|
||||
public override void GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List<RaycastResult> list)
|
||||
{
|
||||
var il2cppList = new Il2CppSystem.Collections.Generic.List<RaycastResult>();
|
||||
|
||||
raycaster.Raycast(data, il2cppList);
|
||||
|
||||
if (il2cppList.Count > 0)
|
||||
list.AddRange(il2cppList.ToArray());
|
||||
}
|
||||
|
||||
// LayerMask.LayerToName
|
||||
|
||||
@ -71,20 +98,19 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
|
||||
internal delegate IntPtr d_FindObjectsOfTypeAll(IntPtr type);
|
||||
|
||||
internal static readonly string[] findObjectsOfTypeAllSignatures = new[]
|
||||
{
|
||||
"UnityEngine.Resources::FindObjectsOfTypeAll",
|
||||
"UnityEngine.ResourcesAPIInternal::FindObjectsOfTypeAll" // Unity 2020+ updated to this
|
||||
};
|
||||
|
||||
public override UnityEngine.Object[] FindObjectsOfTypeAll(Type type)
|
||||
{
|
||||
var iCall = ICallManager.GetICallUnreliable<d_FindObjectsOfTypeAll>(new[]
|
||||
{
|
||||
"UnityEngine.Resources::FindObjectsOfTypeAll",
|
||||
"UnityEngine.ResourcesAPIInternal::FindObjectsOfTypeAll" // Unity 2020+ updated to this
|
||||
});
|
||||
|
||||
return new Il2CppReferenceArray<UnityEngine.Object>(iCall.Invoke(Il2CppType.From(type).Pointer));
|
||||
return new Il2CppReferenceArray<UnityEngine.Object>(
|
||||
ICallManager.GetICallUnreliable<d_FindObjectsOfTypeAll>(findObjectsOfTypeAllSignatures)
|
||||
.Invoke(Il2CppType.From(type).Pointer));
|
||||
}
|
||||
|
||||
public override int GetSceneHandle(Scene scene)
|
||||
=> scene.handle;
|
||||
|
||||
// Scene.GetRootGameObjects();
|
||||
|
||||
internal delegate void d_GetRootGameObjects(int handle, IntPtr list);
|
||||
@ -94,22 +120,17 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
if (!scene.isLoaded)
|
||||
return new GameObject[0];
|
||||
|
||||
int handle = scene.handle;
|
||||
|
||||
if (handle == -1)
|
||||
if (scene.handle == -1)
|
||||
return new GameObject[0];
|
||||
|
||||
int count = GetRootCount(handle);
|
||||
int count = GetRootCount(scene.handle);
|
||||
|
||||
if (count < 1)
|
||||
return new GameObject[0];
|
||||
|
||||
var list = new Il2CppSystem.Collections.Generic.List<GameObject>(count);
|
||||
|
||||
var iCall = ICallManager.GetICall<d_GetRootGameObjects>("UnityEngine.SceneManagement.Scene::GetRootGameObjectsInternal");
|
||||
|
||||
iCall.Invoke(handle, list.Pointer);
|
||||
|
||||
iCall.Invoke(scene.handle, list.Pointer);
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
@ -125,42 +146,95 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
.Invoke(handle);
|
||||
}
|
||||
|
||||
internal static bool? s_doPropertiesExist;
|
||||
internal static bool triedToGetColorBlockProps;
|
||||
internal static PropertyInfo _normalColorProp;
|
||||
internal static PropertyInfo _highlightColorProp;
|
||||
internal static PropertyInfo _pressedColorProp;
|
||||
internal static PropertyInfo _disabledColorProp;
|
||||
|
||||
public override ColorBlock SetColorBlock(ColorBlock colors, Color? normal = null, Color? highlighted = null, Color? pressed = null)
|
||||
public override void SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null,
|
||||
Color? disabled = null)
|
||||
{
|
||||
if (s_doPropertiesExist == null)
|
||||
{
|
||||
var prop = ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "normalColor") as PropertyInfo;
|
||||
s_doPropertiesExist = prop != null && prop.CanWrite;
|
||||
}
|
||||
var colors = selectable.colors;
|
||||
|
||||
colors.colorMultiplier = 1;
|
||||
|
||||
object boxed = (object)colors;
|
||||
|
||||
if (s_doPropertiesExist == true)
|
||||
if (!triedToGetColorBlockProps)
|
||||
{
|
||||
if (normal != null)
|
||||
ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "normalColor").SetValue(boxed, (Color)normal);
|
||||
if (pressed != null)
|
||||
ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "pressedColor").SetValue(boxed, (Color)pressed);
|
||||
if (highlighted != null)
|
||||
ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "highlightedColor").SetValue(boxed, (Color)highlighted);
|
||||
triedToGetColorBlockProps = true;
|
||||
|
||||
if (ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "normalColor") is PropertyInfo norm && norm.CanWrite)
|
||||
_normalColorProp = norm;
|
||||
if (ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "highlightedColor") is PropertyInfo high && high.CanWrite)
|
||||
_highlightColorProp = high;
|
||||
if (ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "pressedColor") is PropertyInfo pres && pres.CanWrite)
|
||||
_pressedColorProp = pres;
|
||||
if (ReflectionUtility.GetPropertyInfo(typeof(ColorBlock), "disabledColor") is PropertyInfo disa && disa.CanWrite)
|
||||
_disabledColorProp = disa;
|
||||
}
|
||||
else if (s_doPropertiesExist == false)
|
||||
|
||||
try
|
||||
{
|
||||
if (normal != null)
|
||||
ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_NormalColor").SetValue(boxed, (Color)normal);
|
||||
if (pressed != null)
|
||||
ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_PressedColor").SetValue(boxed, (Color)pressed);
|
||||
{
|
||||
if (_normalColorProp != null)
|
||||
_normalColorProp.SetValue(boxed, (Color)normal);
|
||||
else if (ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_NormalColor") is FieldInfo fi)
|
||||
fi.SetValue(boxed, (Color)normal);
|
||||
}
|
||||
|
||||
if (highlighted != null)
|
||||
ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_HighlightedColor").SetValue(boxed, (Color)highlighted);
|
||||
{
|
||||
if (_highlightColorProp != null)
|
||||
_highlightColorProp.SetValue(boxed, (Color)highlighted);
|
||||
else if (ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_HighlightedColor") is FieldInfo fi)
|
||||
fi.SetValue(boxed, (Color)highlighted);
|
||||
}
|
||||
|
||||
if (pressed != null)
|
||||
{
|
||||
if (_pressedColorProp != null)
|
||||
_pressedColorProp.SetValue(boxed, (Color)pressed);
|
||||
else if (ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_PressedColor") is FieldInfo fi)
|
||||
fi.SetValue(boxed, (Color)pressed);
|
||||
}
|
||||
|
||||
if (disabled != null)
|
||||
{
|
||||
if (_disabledColorProp != null)
|
||||
_disabledColorProp.SetValue(boxed, (Color)disabled);
|
||||
else if (ReflectionUtility.GetFieldInfo(typeof(ColorBlock), "m_DisabledColor") is FieldInfo fi)
|
||||
fi.SetValue(boxed, (Color)disabled);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log(ex);
|
||||
}
|
||||
|
||||
colors = (ColorBlock)boxed;
|
||||
|
||||
return colors;
|
||||
SetColorBlock(selectable, colors);
|
||||
}
|
||||
|
||||
public override void SetColorBlock(Selectable selectable, ColorBlock _colorBlock)
|
||||
{
|
||||
try
|
||||
{
|
||||
selectable = selectable.TryCast<Selectable>();
|
||||
|
||||
ReflectionUtility.GetPropertyInfo(typeof(Selectable), "m_Colors")
|
||||
.SetValue(selectable, _colorBlock, null);
|
||||
|
||||
ReflectionUtility.GetMethodInfo(typeof(Selectable), "OnSetProperty")
|
||||
.Invoke(selectable, ArgumentUtility.EmptyArgs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,6 +251,16 @@ public static class Il2CppExtensions
|
||||
action.AddListener(listener);
|
||||
}
|
||||
|
||||
public static void RemoveListener(this UnityEvent action, Action listener)
|
||||
{
|
||||
action.RemoveListener(listener);
|
||||
}
|
||||
|
||||
public static void RemoveListener<T>(this UnityEvent<T> action, Action<T> listener)
|
||||
{
|
||||
action.RemoveListener(listener);
|
||||
}
|
||||
|
||||
public static void SetChildControlHeight(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlHeight = value;
|
||||
public static void SetChildControlWidth(this HorizontalOrVerticalLayoutGroup group, bool value) => group.childControlWidth = value;
|
||||
}
|
||||
|
@ -1,373 +0,0 @@
|
||||
#if CPP
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnhollowerBaseLib;
|
||||
using UnhollowerRuntimeLib;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using UnityExplorer.Core;
|
||||
using CppType = Il2CppSystem.Type;
|
||||
using BF = System.Reflection.BindingFlags;
|
||||
|
||||
namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
{
|
||||
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "External methods")]
|
||||
public class Il2CppReflection : ReflectionProvider
|
||||
{
|
||||
public Il2CppReflection() : base()
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
TryLoadGameModules();
|
||||
}
|
||||
|
||||
public override object Cast(object obj, Type castTo)
|
||||
{
|
||||
return Il2CppCast(obj, castTo);
|
||||
}
|
||||
|
||||
public override string ProcessTypeNameInString(Type type, string theString, ref string typeName)
|
||||
{
|
||||
if (!Il2CppTypeNotNull(type))
|
||||
return theString;
|
||||
|
||||
var cppType = Il2CppType.From(type);
|
||||
if (cppType != null && s_deobfuscatedTypeNames.ContainsKey(cppType.FullName))
|
||||
{
|
||||
typeName = s_deobfuscatedTypeNames[cppType.FullName];
|
||||
theString = theString.Replace(cppType.FullName, typeName);
|
||||
}
|
||||
|
||||
return theString;
|
||||
}
|
||||
|
||||
public override Type GetActualType(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var type = obj.GetType();
|
||||
|
||||
if (obj is Il2CppSystem.Object cppObject)
|
||||
{
|
||||
// weird specific case - if the object is an Il2CppSystem.Type, then return so manually.
|
||||
if (cppObject is CppType)
|
||||
return typeof(CppType);
|
||||
|
||||
if (!string.IsNullOrEmpty(type.Namespace))
|
||||
{
|
||||
// Il2CppSystem-namespace objects should just return GetType,
|
||||
// because using GetIl2CppType returns the System namespace type instead.
|
||||
if (type.Namespace.StartsWith("System.") || type.Namespace.StartsWith("Il2CppSystem."))
|
||||
return cppObject.GetType();
|
||||
}
|
||||
|
||||
var cppType = cppObject.GetIl2CppType();
|
||||
|
||||
// check if type is injected
|
||||
IntPtr classPtr = il2cpp_object_get_class(cppObject.Pointer);
|
||||
if (RuntimeSpecificsStore.IsInjected(classPtr))
|
||||
{
|
||||
var typeByName = ReflectionUtility.GetTypeByName(cppType.FullName);
|
||||
if (typeByName != null)
|
||||
return typeByName;
|
||||
}
|
||||
|
||||
// this should be fine for all other il2cpp objects
|
||||
var getType = GetMonoType(cppType);
|
||||
if (getType != null)
|
||||
return getType;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
// caching for GetMonoType
|
||||
private static readonly Dictionary<string, Type> Il2CppToMonoType = new Dictionary<string, Type>();
|
||||
|
||||
// keep deobfuscated type name cache, used to display proper name.
|
||||
internal static Dictionary<string, string> s_deobfuscatedTypeNames = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the Mono (Unhollowed) Type representation of the provided <see cref="Il2CppSystem.Type"/>.
|
||||
/// </summary>
|
||||
/// <param name="cppType">The Cpp Type you want to convert to Mono.</param>
|
||||
/// <returns>The Mono Type if found, otherwise null.</returns>
|
||||
public static Type GetMonoType(CppType cppType)
|
||||
{
|
||||
string name = cppType.AssemblyQualifiedName;
|
||||
|
||||
if (Il2CppToMonoType.ContainsKey(name))
|
||||
return Il2CppToMonoType[name];
|
||||
|
||||
Type ret = Type.GetType(name);
|
||||
|
||||
// Thanks to Slaynash for this deobfuscation snippet!
|
||||
if (ret == null)
|
||||
{
|
||||
string baseName = cppType.FullName;
|
||||
string baseAssembly = cppType.Assembly.GetName().name;
|
||||
|
||||
ret = AppDomain.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.FirstOrDefault(a
|
||||
=> a.GetName().Name == baseAssembly)?
|
||||
.TryGetTypes()
|
||||
.FirstOrDefault(t
|
||||
=> t.CustomAttributes.Any(ca
|
||||
=> ca.AttributeType.Name == "ObfuscatedNameAttribute"
|
||||
&& (string)ca.ConstructorArguments[0].Value == baseName));
|
||||
|
||||
if (ret != null)
|
||||
{
|
||||
// deobfuscated type was found, add to cache.
|
||||
s_deobfuscatedTypeNames.Add(cppType.FullName, ret.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
Il2CppToMonoType.Add(name, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// cached class pointers for Il2CppCast
|
||||
private static readonly Dictionary<string, IntPtr> s_cppClassPointers = new Dictionary<string, IntPtr>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to cast the object to its underlying type.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object you want to cast.</param>
|
||||
/// <returns>The object, as the underlying type if successful or the input value if not.</returns>
|
||||
public static object Il2CppCast(object obj) => Il2CppCast(obj, Instance.GetActualType(obj));
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to cast the object to the provided type.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object you want to cast.</param>
|
||||
/// <param name="castTo">The Type you want to cast to.</param>
|
||||
/// <returns>The object, as the type (or a normal C# object) if successful or the input value if not.</returns>
|
||||
public static object Il2CppCast(object obj, Type castTo)
|
||||
{
|
||||
if (!(obj is Il2CppSystem.Object cppObj))
|
||||
return obj;
|
||||
|
||||
if (!Il2CppTypeNotNull(castTo, out IntPtr castToPtr))
|
||||
return obj;
|
||||
|
||||
IntPtr castFromPtr = il2cpp_object_get_class(cppObj.Pointer);
|
||||
|
||||
if (!il2cpp_class_is_assignable_from(castToPtr, castFromPtr))
|
||||
return null;
|
||||
|
||||
if (RuntimeSpecificsStore.IsInjected(castToPtr))
|
||||
return UnhollowerBaseLib.Runtime.ClassInjectorBase.GetMonoObjectFromIl2CppPointer(cppObj.Pointer);
|
||||
|
||||
if (castTo == typeof(string))
|
||||
return cppObj.ToString();
|
||||
|
||||
return Activator.CreateInstance(castTo, cppObj.Pointer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type.
|
||||
/// </summary>
|
||||
/// <param name="type">The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for.</param>
|
||||
/// <returns>True if successful, false if not.</returns>
|
||||
public static bool Il2CppTypeNotNull(Type type) => Il2CppTypeNotNull(type, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Get the Il2Cpp Class Pointer for the provided Mono (Unhollowed) Type.
|
||||
/// </summary>
|
||||
/// <param name="type">The Mono/Unhollowed Type you want the Il2Cpp Class Pointer for.</param>
|
||||
/// <param name="il2cppPtr">The IntPtr for the Il2Cpp class, or IntPtr.Zero if not found.</param>
|
||||
/// <returns>True if successful, false if not.</returns>
|
||||
public static bool Il2CppTypeNotNull(Type type, out IntPtr il2cppPtr)
|
||||
{
|
||||
if (s_cppClassPointers.TryGetValue(type.AssemblyQualifiedName, out il2cppPtr))
|
||||
return il2cppPtr != IntPtr.Zero;
|
||||
|
||||
il2cppPtr = (IntPtr)typeof(Il2CppClassPointerStore<>)
|
||||
.MakeGenericType(new Type[] { type })
|
||||
.GetField("NativeClassPtr", BF.Public | BF.Static)
|
||||
.GetValue(null);
|
||||
|
||||
s_cppClassPointers.Add(type.AssemblyQualifiedName, il2cppPtr);
|
||||
|
||||
return il2cppPtr != IntPtr.Zero;
|
||||
}
|
||||
|
||||
// Extern C++ methods
|
||||
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern bool il2cpp_class_is_assignable_from(IntPtr klass, IntPtr oklass);
|
||||
|
||||
[DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
public static extern IntPtr il2cpp_object_get_class(IntPtr obj);
|
||||
|
||||
internal static IntPtr s_cppEnumerableClassPtr;
|
||||
internal static IntPtr s_cppDictionaryClassPtr;
|
||||
|
||||
public override bool IsAssignableFrom(Type toAssignTo, Type toAssignFrom)
|
||||
{
|
||||
if (toAssignTo.IsAssignableFrom(toAssignFrom))
|
||||
return true;
|
||||
|
||||
if (toAssignTo == typeof(IEnumerable))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_cppEnumerableClassPtr == IntPtr.Zero)
|
||||
Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IEnumerable), out s_cppEnumerableClassPtr);
|
||||
|
||||
if (s_cppEnumerableClassPtr != IntPtr.Zero
|
||||
&& Il2CppTypeNotNull(toAssignFrom, out IntPtr assignFromPtr)
|
||||
&& il2cpp_class_is_assignable_from(s_cppEnumerableClassPtr, assignFromPtr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else if (toAssignTo == typeof(IDictionary))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_cppDictionaryClassPtr == IntPtr.Zero)
|
||||
if (!Il2CppTypeNotNull(typeof(Il2CppSystem.Collections.IDictionary), out s_cppDictionaryClassPtr))
|
||||
return false;
|
||||
|
||||
if (Il2CppTypeNotNull(toAssignFrom, out IntPtr classPtr))
|
||||
{
|
||||
if (il2cpp_class_is_assignable_from(s_cppDictionaryClassPtr, classPtr))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsReflectionSupported(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
var gArgs = type.GetGenericArguments();
|
||||
if (!gArgs.Any())
|
||||
return true;
|
||||
|
||||
foreach (var gType in gArgs)
|
||||
{
|
||||
if (!Supported(gType))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
bool Supported(Type t)
|
||||
{
|
||||
if (!typeof(Il2CppSystem.Object).IsAssignableFrom(t))
|
||||
return true;
|
||||
|
||||
if (!Il2CppTypeNotNull(t, out IntPtr ptr))
|
||||
return false;
|
||||
|
||||
return CppType.internal_from_handle(IL2CPP.il2cpp_class_get_type(ptr)) is CppType;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for IL2CPP to try to make sure the Unhollowed game assemblies are actually loaded.
|
||||
|
||||
internal static void TryLoadGameModules()
|
||||
{
|
||||
Instance.LoadModule("Assembly-CSharp");
|
||||
Instance.LoadModule("Assembly-CSharp-firstpass");
|
||||
}
|
||||
|
||||
public override bool LoadModule(string module)
|
||||
{
|
||||
#if ML
|
||||
var path = Path.Combine("MelonLoader", "Managed", $"{module}.dll");
|
||||
#else
|
||||
var path = Path.Combine("BepInEx", "unhollowed", $"{module}.dll");
|
||||
#endif
|
||||
return LoadModuleInternal(path);
|
||||
}
|
||||
|
||||
internal static bool LoadModuleInternal(string fullPath)
|
||||
{
|
||||
if (!File.Exists(fullPath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Assembly.Load(File.ReadAllBytes(fullPath));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.GetType() + ", " + e.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void BoxStringToType(ref object value, Type castTo)
|
||||
{
|
||||
if (castTo == typeof(Il2CppSystem.String))
|
||||
value = (Il2CppSystem.String)(value as string);
|
||||
else
|
||||
value = (Il2CppSystem.Object)(value as string);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~ not used ~~~~~~~~~~~~
|
||||
|
||||
// cached il2cpp unbox methods
|
||||
internal static readonly Dictionary<string, MethodInfo> s_unboxMethods = new Dictionary<string, MethodInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to unbox the object to the underlying struct type.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object which is a struct underneath.</param>
|
||||
/// <returns>The struct if successful, otherwise null.</returns>
|
||||
public static object Unbox(object obj) => Unbox(obj, Instance.GetActualType(obj));
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to unbox the object to the struct type.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object which is a struct underneath.</param>
|
||||
/// <param name="type">The type of the struct you want to unbox to.</param>
|
||||
/// <returns>The struct if successful, otherwise null.</returns>
|
||||
public static object Unbox(object obj, Type type)
|
||||
{
|
||||
if (!type.IsValueType)
|
||||
return null;
|
||||
|
||||
if (!(obj is Il2CppSystem.Object))
|
||||
return obj;
|
||||
|
||||
var name = type.AssemblyQualifiedName;
|
||||
|
||||
if (!s_unboxMethods.ContainsKey(name))
|
||||
{
|
||||
s_unboxMethods.Add(name, typeof(Il2CppObjectBase)
|
||||
.GetMethod("Unbox")
|
||||
.MakeGenericMethod(type));
|
||||
}
|
||||
|
||||
return s_unboxMethods[name].Invoke(obj, new object[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -38,19 +38,6 @@ namespace UnityExplorer.Core.Runtime.Il2Cpp
|
||||
return new Il2CppStructArray<byte>(ptr);
|
||||
}
|
||||
|
||||
// bool ImageConversion.LoadImage(this Texture2D tex, byte[] data, bool markNonReadable);
|
||||
|
||||
internal delegate bool d_LoadImage(IntPtr tex, IntPtr data, bool markNonReadable);
|
||||
|
||||
public override bool LoadImage(Texture2D tex, byte[] data, bool markNonReadable)
|
||||
{
|
||||
var il2cppArray = (Il2CppStructArray<byte>)data;
|
||||
|
||||
var iCall = ICallManager.GetICall<d_LoadImage>("UnityEngine.ImageConversion::LoadImage");
|
||||
|
||||
return iCall.Invoke(tex.Pointer, il2cppArray.Pointer, markNonReadable);
|
||||
}
|
||||
|
||||
// Sprite Sprite.Create
|
||||
|
||||
public override Sprite CreateSprite(Texture2D texture)
|
||||
|
@ -7,10 +7,10 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using UnityExplorer.Core;
|
||||
using UnityExplorer.Core.CSharp;
|
||||
using UnityExplorer;
|
||||
|
||||
namespace UnityExplorer.Core.Runtime.Mono
|
||||
{
|
||||
@ -18,7 +18,7 @@ namespace UnityExplorer.Core.Runtime.Mono
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
Reflection = new MonoReflection();
|
||||
ExplorerCore.Context = RuntimeContext.Mono;
|
||||
TextureUtil = new MonoTextureUtil();
|
||||
}
|
||||
|
||||
@ -28,43 +28,41 @@ namespace UnityExplorer.Core.Runtime.Mono
|
||||
}
|
||||
|
||||
private void Application_logMessageReceived(string condition, string stackTrace, LogType type)
|
||||
=> ExplorerCore.LogUnity(condition, type);
|
||||
|
||||
public override void StartCoroutine(IEnumerator routine)
|
||||
=> ExplorerBehaviour.Instance.StartCoroutine(routine);
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
ExplorerCore.Log(condition, type, true);
|
||||
}
|
||||
|
||||
public override void StartConsoleCoroutine(IEnumerator routine)
|
||||
{
|
||||
DummyBehaviour.Instance.StartCoroutine(routine);
|
||||
}
|
||||
public override T AddComponent<T>(GameObject obj, Type type)
|
||||
=> (T)obj.AddComponent(type);
|
||||
|
||||
public override string LayerToName(int layer)
|
||||
public override ScriptableObject CreateScriptable(Type type)
|
||||
=> ScriptableObject.CreateInstance(type);
|
||||
|
||||
public override void GraphicRaycast(GraphicRaycaster raycaster, PointerEventData data, List<RaycastResult> list)
|
||||
=> raycaster.Raycast(data, list);
|
||||
|
||||
public override string LayerToName(int layer)
|
||||
=> LayerMask.LayerToName(layer);
|
||||
|
||||
public override UnityEngine.Object[] FindObjectsOfTypeAll(Type type)
|
||||
public override UnityEngine.Object[] FindObjectsOfTypeAll(Type type)
|
||||
=> Resources.FindObjectsOfTypeAll(type);
|
||||
|
||||
private static readonly FieldInfo fi_Scene_handle = typeof(Scene).GetField("m_Handle", ReflectionUtility.AllFlags);
|
||||
public override GameObject[] GetRootGameObjects(Scene scene)
|
||||
=> scene.isLoaded ? scene.GetRootGameObjects() : new GameObject[0];
|
||||
|
||||
public override int GetSceneHandle(Scene scene)
|
||||
public override int GetRootCount(Scene scene)
|
||||
=> scene.rootCount;
|
||||
|
||||
public override void SetColorBlock(Selectable selectable, Color? normal = null, Color? highlighted = null, Color? pressed = null,
|
||||
Color? disabled = null)
|
||||
{
|
||||
return (int)fi_Scene_handle.GetValue(scene);
|
||||
}
|
||||
var colors = selectable.colors;
|
||||
|
||||
public override GameObject[] GetRootGameObjects(Scene scene)
|
||||
{
|
||||
if (!scene.isLoaded)
|
||||
return new GameObject[0];
|
||||
|
||||
return scene.GetRootGameObjects();
|
||||
}
|
||||
|
||||
public override int GetRootCount(Scene scene)
|
||||
{
|
||||
return scene.rootCount;
|
||||
}
|
||||
|
||||
public override ColorBlock SetColorBlock(ColorBlock colors, Color? normal = null, Color? highlighted = null, Color? pressed = null)
|
||||
{
|
||||
if (normal != null)
|
||||
colors.normalColor = (Color)normal;
|
||||
|
||||
@ -74,47 +72,48 @@ namespace UnityExplorer.Core.Runtime.Mono
|
||||
if (pressed != null)
|
||||
colors.pressedColor = (Color)pressed;
|
||||
|
||||
return colors;
|
||||
if (disabled != null)
|
||||
colors.disabledColor = (Color)disabled;
|
||||
|
||||
SetColorBlock(selectable, colors);
|
||||
}
|
||||
|
||||
public override void SetColorBlock(Selectable selectable, ColorBlock colors)
|
||||
=> selectable.colors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MonoExtensions
|
||||
{
|
||||
// Helpers to use the same style of AddListener that IL2CPP uses.
|
||||
|
||||
public static void AddListener(this UnityEvent _event, Action listener)
|
||||
{
|
||||
_event.AddListener(new UnityAction(listener));
|
||||
}
|
||||
=> _event.AddListener(new UnityAction(listener));
|
||||
|
||||
public static void AddListener<T>(this UnityEvent<T> _event, Action<T> listener)
|
||||
{
|
||||
_event.AddListener(new UnityAction<T>(listener));
|
||||
}
|
||||
=> _event.AddListener(new UnityAction<T>(listener));
|
||||
|
||||
public static void Clear(this StringBuilder sb)
|
||||
{
|
||||
sb.Remove(0, sb.Length);
|
||||
}
|
||||
public static void RemoveListener(this UnityEvent _event, Action listener)
|
||||
=> _event.RemoveListener(new UnityAction(listener));
|
||||
|
||||
private static PropertyInfo pi_childControlHeight;
|
||||
public static void RemoveListener<T>(this UnityEvent<T> _event, Action<T> listener)
|
||||
=> _event.RemoveListener(new UnityAction<T>(listener));
|
||||
|
||||
// Doesn't exist in NET 3.5
|
||||
|
||||
public static void Clear(this StringBuilder sb)
|
||||
=> sb.Remove(0, sb.Length);
|
||||
|
||||
// These properties don't exist in some earlier games, so null check before trying to set them.
|
||||
|
||||
public static void SetChildControlHeight(this HorizontalOrVerticalLayoutGroup group, bool value)
|
||||
{
|
||||
if (pi_childControlHeight == null)
|
||||
pi_childControlHeight = group.GetType().GetProperty("childControlHeight");
|
||||
|
||||
pi_childControlHeight?.SetValue(group, value, null);
|
||||
}
|
||||
=> ReflectionUtility.GetPropertyInfo(typeof(HorizontalOrVerticalLayoutGroup), "childControlHeight")
|
||||
?.SetValue(group, value, null);
|
||||
|
||||
private static PropertyInfo pi_childControlWidth;
|
||||
|
||||
public static void SetChildControlWidth(this HorizontalOrVerticalLayoutGroup group, bool value)
|
||||
{
|
||||
if (pi_childControlWidth == null)
|
||||
pi_childControlWidth = group.GetType().GetProperty("childControlWidth");
|
||||
|
||||
pi_childControlWidth?.SetValue(group, value, null);
|
||||
}
|
||||
=> ReflectionUtility.GetPropertyInfo(typeof(HorizontalOrVerticalLayoutGroup), "childControlWidth")
|
||||
?.SetValue(group, value, null);
|
||||
}
|
||||
|
||||
#endif
|
@ -1,36 +0,0 @@
|
||||
#if MONO
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace UnityExplorer.Core.Runtime.Mono
|
||||
{
|
||||
public class MonoReflection : ReflectionProvider
|
||||
{
|
||||
// Mono doesn't need to explicitly cast things.
|
||||
public override object Cast(object obj, Type castTo)
|
||||
=> obj;
|
||||
|
||||
// Vanilla GetType is fine for mono
|
||||
public override Type GetActualType(object obj)
|
||||
=> obj.GetType();
|
||||
|
||||
public override bool IsAssignableFrom(Type toAssignTo, Type toAssignFrom)
|
||||
=> toAssignTo.IsAssignableFrom(toAssignFrom);
|
||||
|
||||
public override bool IsReflectionSupported(Type type)
|
||||
=> true;
|
||||
|
||||
public override bool LoadModule(string module)
|
||||
=> true;
|
||||
|
||||
public override string ProcessTypeNameInString(Type type, string theString, ref string typeName)
|
||||
=> theString;
|
||||
|
||||
// not necessary
|
||||
public override void BoxStringToType(ref object _string, Type castTo) { }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user