mirror of
https://github.com/originalnicodr/CinematicUnityExplorer.git
synced 2026-10-10 13:03:15 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 361f10704e | |||
| e6bcbcd0cc | |||
| 1b93dd67f8 | |||
| 733887ea50 | |||
| d042771896 | |||
| df5a1542ba | |||
| 27851a88ea | |||
| 5c556d99c5 | |||
| 91e5bcb38c | |||
| a08a299a8f | |||
| 32bf1a6246 | |||
| e3584654a6 | |||
| 388c9fbc29 | |||
| 2f35b9d2c7 | |||
| b82cc62186 | |||
| 9429b84cfa | |||
| 2719c69753 | |||
| 9144f89e32 | |||
| 3bc1268444 | |||
| 4c100daf83 | |||
| e7d591176e | |||
| ca46607c49 | |||
| 7fb67b4e28 | |||
| c0f42ad983 | |||
| 6111c1743b |
@@ -29,79 +29,79 @@ jobs:
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload BepInEx.IL2CPP
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx.IL2CPP.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx.IL2CPP/
|
||||
|
||||
- name: Upload BepInEx.IL2CPP.CoreCLR
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx.IL2CPP.CoreCLR.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx.IL2CPP.CoreCLR/
|
||||
|
||||
- name: Upload BepInEx.Unity.IL2CPP.CoreCLR
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx.Unity.IL2CPP.CoreCLR.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx.Unity.IL2CPP.CoreCLR/
|
||||
|
||||
- name: Upload BepInEx5.Mono
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx5.Mono.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx5.Mono/
|
||||
|
||||
- name: Upload BepInEx6.Mono
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx6.Mono.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx6.Mono/
|
||||
|
||||
- name: Upload BepInEx6.Unity.Mono
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.BepInEx6.Unity.Mono.zip
|
||||
path: ./Release/CinematicUnityExplorer.BepInEx6.Unity.Mono/
|
||||
|
||||
- name: Upload MelonLoader.IL2CPP
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.MelonLoader.IL2CPP.zip
|
||||
path: ./Release/CinematicUnityExplorer.MelonLoader.IL2CPP/
|
||||
|
||||
- name: Upload MelonLoader.IL2CPP.net6preview
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.MelonLoader.IL2CPP.net6preview.zip
|
||||
path: ./Release/CinematicUnityExplorer.MelonLoader.IL2CPP.net6preview/
|
||||
|
||||
- name: Upload MelonLoader.IL2CPP.CoreCLR
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.MelonLoader.IL2CPP.CoreCLR.zip
|
||||
path: ./Release/CinematicUnityExplorer.MelonLoader.IL2CPP.CoreCLR/
|
||||
|
||||
- name: Upload MelonLoader.Mono
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.MelonLoader.Mono.zip
|
||||
path: ./Release/CinematicUnityExplorer.MelonLoader.Mono/
|
||||
|
||||
- name: Upload Standalone.IL2CPP
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.Standalone.IL2CPP.zip
|
||||
path: ./Release/CinematicUnityExplorer.Standalone.IL2CPP/
|
||||
|
||||
- name: Upload Standalone.Mono
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.Standalone.Mono.zip
|
||||
path: ./Release/CinematicUnityExplorer.Standalone.Mono/
|
||||
|
||||
- name: Upload Editor
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CinematicUnityExplorer.Editor.zip
|
||||
path: ./UnityEditorPackage/
|
||||
@@ -127,13 +127,13 @@ jobs:
|
||||
run: ./build_connector.ps1
|
||||
|
||||
- name: Upload Unity IGCS Connector
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: UnityIGCSConnector.dll
|
||||
path: ./Release/UnityIGCSConnector.dll
|
||||
|
||||
- name: Upload Unity IGCS Connector 32bit
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: UnityIGCSConnector.32.dll
|
||||
path: ./Release/UnityIGCSConnector.32.dll
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
<p align="center">
|
||||
✨ Powered by <a href="https://github.com/yukieiji/UniverseLib">UniverseLib</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
🛠️ Need support? Join our <a href="discord.gg/FqTpKutNah">Discord server</a>!
|
||||
</p>
|
||||
|
||||
# Releases [](../../releases)
|
||||
|
||||
[](../../releases/latest) [](https://github.com/originalnicodr/CinematicUnityExplorer/actions) [](../../releases/latest)
|
||||
[](../../releases/latest) [](https://github.com/originalnicodr/CinematicUnityExplorer/actions) [](../../releases/latest) [](https://discord.gg/FqTpKutNah)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=--8DOKWzGJ4" target="_blank">
|
||||
@@ -100,27 +103,38 @@ I wrote a [guide](https://framedsc.com/GeneralGuides/cinematic-unity-explorer.ht
|
||||
|
||||
The table below describes all of the relevant default hotkeys. You can edit them to your liking by going to the Options panel, changing the hotkeys you want, and saving the settings.
|
||||
|
||||
Feature | Key
|
||||
-|-
|
||||
Freecam | `Insert`
|
||||
Move the camera forward/left/right/backward | `W`/`A`/`S`/`D` - `UpArrow`/`LeftArrow`/`RightArrow`/`DownArrow`
|
||||
Move the camera upwards | `Space`
|
||||
Move the camera downwards | `LeftControl`
|
||||
Speed up movement | `LeftShift`
|
||||
Speed down movement | `LeftAlt`
|
||||
Change freecam orientation | Right mouse click
|
||||
Tilt left | `Numpad 1`
|
||||
Tilt right | `Numpad 3`
|
||||
Reset tilt | `Numpad 2`
|
||||
Increase FOV | `Numpad +`
|
||||
Decrease FOV | `Numpad -`
|
||||
Reset FOV | `Numpad *`
|
||||
Pause | `PageUp`
|
||||
Block Freecam movement and rotation | `Home`
|
||||
Block games input | `Numpad .`
|
||||
Frameskip | `PageDown`
|
||||
HUD Toggle | `Delete`
|
||||
Freeze NPC animations | `Numpad 0`
|
||||
Feature | Keyboard | Gamepad
|
||||
-|-|-
|
||||
Freecam toggle | `Insert` | `R3 + X`
|
||||
Move the camera forward/left/backward/right | `W`/`A`/`S`/`D` - `UpArrow`/`LeftArrow`/`DownArrow`/`RightArrow`/ | Left stick
|
||||
Move the camera upwards | `Space` | `LT`
|
||||
Move the camera downwards | `LeftControl` | `RT`
|
||||
Speed up movement | `LeftShift` | `Y`
|
||||
Speed down movement | `LeftAlt` | `X`
|
||||
Change freecam orientation | Right mouse click | Right stick
|
||||
Tilt left | `Numpad 1` | Dpad left
|
||||
Tilt right | `Numpad 3` | Dpad right
|
||||
Reset tilt | `Numpad 2` | `A`
|
||||
Decrease FOV | `Numpad -` | Dpad up
|
||||
Increase FOV | `Numpad +` | Dpad down
|
||||
Reset FOV | `Numpad *` | `B`
|
||||
Pause | `PageUp` |
|
||||
Block Freecam movement and rotation | `Home` |
|
||||
Block games input | `Numpad .` |
|
||||
Frameskip | `PageDown` |
|
||||
HUD Toggle | `Delete` |
|
||||
Freeze NPC animations | `Numpad 0` |
|
||||
Time scale override toggle | | `L3 + X`
|
||||
Decrease time scale | | `X + L1`
|
||||
Increase time scale | | `X + R1`
|
||||
Camera path: Add node | | `A`
|
||||
Camera path: Navigate to previous node | | `L1`
|
||||
Camera path: Navigate to next node | | `R1`
|
||||
Camera path: Start/stop playback | | `START`
|
||||
Camera path: Pause/unpause playback | | `SELECT`
|
||||
|
||||
> [!NOTE]
|
||||
> Gamepad is only compatible if the game is using [Unity Input System](https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/index.html).
|
||||
|
||||
# Features
|
||||
|
||||
@@ -152,8 +166,8 @@ By default the camera only follows the object's position, but you can also make
|
||||
|
||||
As a side note, you can also "look at" a specific object in the world to force the camera to look directly toward it no matter the camera movement.
|
||||
|
||||
### Game input block for Unity's legacy system
|
||||
Added game input block for Unity's legacy system. You can now block (or unblock) the game's input when using the freecam, as long as the game is using the Unity Legacy Input system. If the game uses a custom solution or the latest Unity system then this won't work. Implementing this for Unity's new system is in the backlog, so if you find a game using it (should say "Initialized new InputSystem support." on the logs) then please let me know so I can implement it using that game!
|
||||
### Game input block for Unity's Legacy and Input System package
|
||||
Added game input block for Unity's Legacy system and Unity Input System package. You can now block (or unblock) the game's input when using the freecam, as long as the game is using either of the mentioned systems. If the game uses a custom solution (or the actions engine system, as far as I understand), then this won't work.
|
||||
|
||||
## Lights manager
|
||||
It allows you to spawn spotlights and pointlights, as well as toggle the game's original lights to allow you to relight the scene however you want (a task that is pretty common for screenshots and lighting artists). Please note that this only turns off scene lights and won't turn off ambient light or lights created by emissive materials, effects, or shaders.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.originalnicodr.cinematicunityexplorer",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"displayName": "CinematicUnityExplorer",
|
||||
"description": "UnityExplorer fork focused on providing tools for creating marketing material for Unity games.",
|
||||
"unity": "2017.1",
|
||||
|
||||
@@ -2,13 +2,30 @@
|
||||
#include <Windows.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <Psapi.h>
|
||||
|
||||
#define EXPOSE __declspec(dllexport)
|
||||
|
||||
// Since we're using C, we're a bit limited on how we define enums,
|
||||
// so for the screenshot session, we have to do this.
|
||||
|
||||
#ifdef _M_IX86
|
||||
typedef uint8_t SessionStatus;
|
||||
#else
|
||||
typedef int SessionStatus;
|
||||
#endif
|
||||
|
||||
#define SESSION_STATUS_OK ((SessionStatus)0)
|
||||
#define SESSION_STATUS_CAMERA_NOT_ENABLED ((SessionStatus)1)
|
||||
#define SESSION_STATUS_CAMERA_PATH_PLAYING ((SessionStatus)2)
|
||||
#define SESSION_STATUS_ALREADY_SESSION_ACTIVE ((SessionStatus)3)
|
||||
#define SESSION_STATUS_CAMERA_FEAT_NOT_AVAILABLE ((SessionStatus)4)
|
||||
#define SESSION_STATUS_UNKNOWN_ERROR ((SessionStatus)5)
|
||||
|
||||
// The function that will be the one called from the C# side, that will update the corresponding values.
|
||||
typedef void (*MoveCameraCallback)(float, float, float, int);
|
||||
|
||||
typedef void (*SessionCallback)(void);
|
||||
typedef int (*SessionCallback)(void);
|
||||
|
||||
typedef uint8_t* (*GetCameraDataFunc)(void);
|
||||
|
||||
@@ -16,15 +33,19 @@ MoveCameraCallback GlobalCallback = NULL;
|
||||
SessionCallback GlobalStartSession = NULL;
|
||||
SessionCallback GlobalEndSession = NULL;
|
||||
|
||||
EXPOSE uint8_t* U_IGCS_CameraData[10];
|
||||
|
||||
// There are things that only needs to be run once.
|
||||
static int first_initialization = 1;
|
||||
|
||||
EXPOSE int __cdecl IGCS_StartScreenshotSession(uint8_t _ignore) {
|
||||
|
||||
|
||||
EXPOSE SessionStatus __cdecl IGCS_StartScreenshotSession(uint8_t _ignore) {
|
||||
if (GlobalStartSession) {
|
||||
GlobalStartSession();
|
||||
printf("Called StartSession\n");
|
||||
return (SessionStatus)GlobalStartSession();
|
||||
}
|
||||
return 0;
|
||||
return SESSION_STATUS_CAMERA_FEAT_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
EXPOSE void __cdecl IGCS_EndScreenshotSession() {
|
||||
@@ -32,6 +53,34 @@ EXPOSE void __cdecl IGCS_EndScreenshotSession() {
|
||||
printf("Called EndSession\n");
|
||||
}
|
||||
|
||||
int connect_all_tools() {
|
||||
HMODULE mods[1024];
|
||||
DWORD cbNeeded;
|
||||
HANDLE proc = GetCurrentProcess();
|
||||
int cameraSize = 0;
|
||||
|
||||
if (!EnumProcessModules(proc, mods, sizeof(mods), &cbNeeded)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
|
||||
FARPROC connect_camera_tools = GetProcAddress(mods[i], "connectFromCameraTools");
|
||||
GetCameraDataFunc getCameraData = (GetCameraDataFunc)GetProcAddress(mods[i], "getDataFromCameraToolsBuffer");
|
||||
|
||||
if (connect_camera_tools == NULL) continue;
|
||||
|
||||
char module_name[512];
|
||||
GetModuleFileNameA(mods[i], module_name, sizeof(module_name));
|
||||
|
||||
printf("Connected to %s tool\n", module_name);
|
||||
connect_camera_tools();
|
||||
U_IGCS_CameraData[cameraSize] = getCameraData();
|
||||
cameraSize++;
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPOSE uint8_t* __cdecl U_IGCS_Initialize(MoveCameraCallback cb, SessionCallback start_cb, SessionCallback end_cb) {
|
||||
AllocConsole();
|
||||
printf("Initializing callback\n");
|
||||
@@ -39,37 +88,18 @@ EXPOSE uint8_t* __cdecl U_IGCS_Initialize(MoveCameraCallback cb, SessionCallback
|
||||
GlobalStartSession = start_cb;
|
||||
GlobalEndSession = end_cb;
|
||||
|
||||
// Load IGCS
|
||||
#ifdef _M_IX86
|
||||
HMODULE igcs = LoadLibraryA("IgcsConnector.addon32");
|
||||
#else
|
||||
HMODULE igcs = LoadLibraryA("IgcsConnector.addon64");
|
||||
#endif
|
||||
|
||||
if (!igcs) {
|
||||
MessageBoxA(
|
||||
NULL,
|
||||
"IgcsConnector.addon64 was not found, make sure it is in the same directory as the executable.",
|
||||
"Unable to find IgcsConnector",
|
||||
MB_OK | MB_ICONERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
FARPROC cameraToolsFunction = GetProcAddress(igcs, "connectFromCameraTools");
|
||||
GetCameraDataFunc getCameraData = (GetCameraDataFunc)GetProcAddress(igcs, "getDataFromCameraToolsBuffer");
|
||||
|
||||
if (first_initialization) {
|
||||
cameraToolsFunction();
|
||||
|
||||
if (connect_all_tools()) {
|
||||
printf("Error connecting cameras\n");
|
||||
return 0;
|
||||
}
|
||||
first_initialization = 0;
|
||||
}
|
||||
|
||||
// TODO: move this where it belongs. Maybe at some point we should actually fill in the data.
|
||||
uint8_t* cameraData = getCameraData();
|
||||
cameraData[0] = 0;
|
||||
|
||||
printf("Camera connected!\n");
|
||||
|
||||
return cameraData;
|
||||
return &U_IGCS_CameraData[0];
|
||||
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule UniverseLib updated: 3dc1aba123...7bf319557c
@@ -1,6 +1,7 @@
|
||||
using HarmonyLib;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Config;
|
||||
#if UNHOLLOWER
|
||||
using IL2CPPUtils = UnhollowerBaseLib.UnhollowerUtils;
|
||||
#endif
|
||||
@@ -12,8 +13,12 @@ namespace UnityExplorer
|
||||
{
|
||||
public class ArrowGenerator
|
||||
{
|
||||
public static GameObject CreateArrow(Vector3 arrowPosition, Quaternion arrowRotation, Color color){
|
||||
public static GameObject CreateArrow(Vector3 arrowPosition, Quaternion arrowRotation, Color color)
|
||||
{
|
||||
try {
|
||||
float arrowSizeValue = ConfigManager.Arrow_Size.Value;
|
||||
Vector3 arrowSize = new Vector3(Math.Max(arrowSizeValue, 0.1f), Math.Max(arrowSizeValue, 0.1f), Math.Max(arrowSizeValue, 0.1f));
|
||||
|
||||
GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
cylinder.GetComponent<Collider>().enabled = false;
|
||||
cylinder.GetComponent<MeshFilter>().mesh = CreateCylinderMesh(0.01f, 20, 2);
|
||||
@@ -34,6 +39,7 @@ namespace UnityExplorer
|
||||
|
||||
GameObject arrow = new GameObject("CUE-Arrow");
|
||||
cylinder.transform.SetParent(arrow.transform, true);
|
||||
arrow.transform.localScale = arrowSize;
|
||||
arrow.transform.position = arrowPosition;
|
||||
arrow.transform.rotation = arrowRotation;
|
||||
arrow.transform.position += 0.5f * arrow.transform.forward; // Move the arrow forward so the cylinder starts on the wanted position
|
||||
|
||||
@@ -87,7 +87,9 @@ namespace UnityExplorer.UI.Panels
|
||||
ButtonRef destroyButton = UIFactory.CreateButton(UIRoot, "Delete", "Delete");
|
||||
UIFactory.SetLayoutElement(destroyButton.GameObject, minWidth: 30, minHeight: 25, flexibleWidth: 9999);
|
||||
destroyButton.OnClick += () => {
|
||||
int deletedIndex = GetCamPathsPanel().controlPoints.IndexOf(point);
|
||||
GetCamPathsPanel().controlPoints.Remove(point);
|
||||
GetCamPathsPanel().OnNodeDeleted(deletedIndex);
|
||||
GetCamPathsPanel().nodesScrollPool.Refresh(true, false);
|
||||
GetCamPathsPanel().MaybeRedrawPath();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using UnityExplorer.Config;
|
||||
using UniverseLib.UI;
|
||||
using UniverseLib.UI.Models;
|
||||
using UniverseLib.UI.Widgets.ScrollView;
|
||||
@@ -69,6 +70,9 @@ namespace UnityExplorer.UI.Panels
|
||||
|
||||
private void ToggleVisualizer(){
|
||||
GameObject visualizer = light.transform.GetChild(0).gameObject;
|
||||
float arrowSize = ConfigManager.Arrow_Size.Value;
|
||||
Vector3 arrowSizeVec = new Vector3(Math.Max(arrowSize, 0.1f), Math.Max(arrowSize, 0.1f), Math.Max(arrowSize, 0.1f));
|
||||
light.transform.GetChild(0).localScale = arrowSizeVec;
|
||||
visualizer.SetActive(!visualizer.activeSelf);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,970 @@
|
||||
// Uncomment to enable verbose logging in gamepad interceptor
|
||||
//#define IGAMEPAD_DEBUG
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityExplorer;
|
||||
using UnityExplorer.UI.Panels;
|
||||
|
||||
#if UNHOLLOWER
|
||||
using IL2CPPUtils = UnhollowerBaseLib.UnhollowerUtils;
|
||||
#endif
|
||||
#if INTEROP
|
||||
using IL2CPPUtils = Il2CppInterop.Common.Il2CppInteropUtils;
|
||||
#endif
|
||||
|
||||
namespace UniverseLib.Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Reflection wrapper to interact with gamepad inputs while blocking their actual state from the game.
|
||||
/// </summary>
|
||||
public static class IGamepadInputInterceptor
|
||||
{
|
||||
// --- Configuration ---
|
||||
private static bool _isEnabled = false;
|
||||
private static int _targetGamepadIndex = 0;
|
||||
|
||||
// --- Reflected types ---
|
||||
private static Type _gamepadType;
|
||||
private static Type _buttonControlType;
|
||||
private static Type _axisControlType;
|
||||
private static Type _vector2ControlType;
|
||||
private static Type _inputControlType;
|
||||
private static PropertyInfo _basePathProp;
|
||||
|
||||
// --- Reflected properties and methods for triggering state capture ---
|
||||
private static PropertyInfo _buttonIsPressedProp;
|
||||
private static PropertyInfo _buttonWasPressedProp;
|
||||
private static PropertyInfo _buttonWasReleasedProp;
|
||||
private static MethodInfo _axisReadValueMethod;
|
||||
private static MethodInfo _vector2ReadValueMethod;
|
||||
|
||||
// --- Cached reflection for device checking ---
|
||||
private static PropertyInfo _deviceProp;
|
||||
|
||||
// --- Cached reflection for Vector2 property access ---
|
||||
private static PropertyInfo _vector2XProp;
|
||||
private static PropertyInfo _vector2YProp;
|
||||
|
||||
// --- Cached reflection for gamepad control access ---
|
||||
private static PropertyInfo _gamepadAllProp;
|
||||
private static PropertyInfo _gamepadCurrentProp;
|
||||
private static PropertyInfo _gamepadLeftStickProp;
|
||||
private static PropertyInfo _gamepadRightStickProp;
|
||||
|
||||
// --- Vendor name normalization ---
|
||||
private static readonly Dictionary<string, string> _vendorPrefixMap = new()
|
||||
{
|
||||
{ "/xinputcontrollerwindows", "/gamepad" },
|
||||
{ "/xinputcontroller", "/gamepad" },
|
||||
{ "/dualshockgamepad", "/gamepad" },
|
||||
{ "/dualsensegamepad", "/gamepad" },
|
||||
{ "/dualshock4gamepadhid", "/gamepad" },
|
||||
{ "/dualshock3gamepadhid", "/gamepad" },
|
||||
{ "/androidgamepad", "/gamepad" },
|
||||
{ "/switchprocontrollerhid", "/gamepad" },
|
||||
};
|
||||
|
||||
// --- Control collections ---
|
||||
// Maps normalized paths to control objects
|
||||
private static readonly Dictionary<string, object> _axisControls = new();
|
||||
private static readonly Dictionary<string, object> _vector2Controls = new();
|
||||
|
||||
// --- State dictionaries (populated by postfix patches) ---
|
||||
private static readonly Dictionary<string, float> _axisValues = new();
|
||||
private static readonly Dictionary<string, Vector2> _vector2Values = new();
|
||||
|
||||
// --- Initialization ---
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the gamepad input interceptor. Must be called during game startup.
|
||||
/// </summary>
|
||||
public static void Init()
|
||||
{
|
||||
try
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] === Initializing ===");
|
||||
#endif
|
||||
|
||||
LoadReflectedTypes();
|
||||
if (_gamepadType == null || _axisControlType == null)
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Failed to load required types");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Only patch analog controls (buttons are handled by INewInputSystem)
|
||||
PatchAnalogControls();
|
||||
//INewInputSystem.Init() should have been run beforehand
|
||||
DiscoverGamepadControls();
|
||||
_isEnabled = true;
|
||||
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] === Initialization complete ===");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Init failed: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables input interception. When enabled, captured inputs will be replaced with default values.
|
||||
/// </summary>
|
||||
public static void Enable()
|
||||
{
|
||||
_isEnabled = true;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Input interception enabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables input interception. Game will receive normal gamepad input.
|
||||
/// </summary>
|
||||
public static void Disable()
|
||||
{
|
||||
_isEnabled = false;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Input interception disabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets which gamepad to target when multiple gamepads are connected.
|
||||
/// </summary>
|
||||
public static void SetTargetGamepad(int index)
|
||||
{
|
||||
_targetGamepadIndex = index;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Target gamepad set to index {index}");
|
||||
#endif
|
||||
// Re-discover controls for the new gamepad
|
||||
ClearControlCaches();
|
||||
DiscoverGamepadControls();
|
||||
}
|
||||
|
||||
// --- Type Loading ---
|
||||
|
||||
private static void LoadReflectedTypes()
|
||||
{
|
||||
_gamepadType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Gamepad, Unity.InputSystem");
|
||||
_buttonControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.ButtonControl, Unity.InputSystem");
|
||||
_axisControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.AxisControl, Unity.InputSystem");
|
||||
_vector2ControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.Vector2Control, Unity.InputSystem");
|
||||
_inputControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputControl, Unity.InputSystem");
|
||||
|
||||
_basePathProp = _inputControlType?.GetProperty("path", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
_buttonIsPressedProp = _buttonControlType?.GetProperty("isPressed", BindingFlags.Public | BindingFlags.Instance);
|
||||
_buttonWasPressedProp = _buttonControlType?.GetProperty("wasPressedThisFrame", BindingFlags.Public | BindingFlags.Instance);
|
||||
_buttonWasReleasedProp = _buttonControlType?.GetProperty("wasReleasedThisFrame", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
_axisReadValueMethod = _axisControlType?.GetMethod("ReadValue", Type.EmptyTypes);
|
||||
_vector2ReadValueMethod = _vector2ControlType?.GetMethod("ReadValue", Type.EmptyTypes);
|
||||
|
||||
_deviceProp = _inputControlType?.GetProperty("device", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
_vector2XProp = _vector2ControlType?.GetProperty("x", BindingFlags.Public | BindingFlags.Instance);
|
||||
_vector2YProp = _vector2ControlType?.GetProperty("y", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
_gamepadAllProp = _gamepadType?.GetProperty("all", BindingFlags.Static | BindingFlags.Public);
|
||||
_gamepadCurrentProp = _gamepadType?.GetProperty("current", BindingFlags.Static | BindingFlags.Public);
|
||||
_gamepadLeftStickProp = _gamepadType?.GetProperty("leftStick", BindingFlags.Public | BindingFlags.Instance);
|
||||
_gamepadRightStickProp = _gamepadType?.GetProperty("rightStick", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
#if IGAMEPAD_DEBUG
|
||||
LogLoadedType("Gamepad", _gamepadType);
|
||||
LogLoadedType("ButtonControl", _buttonControlType);
|
||||
LogLoadedType("AxisControl", _axisControlType);
|
||||
LogLoadedType("Vector2Control", _vector2ControlType);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void LogLoadedType(string name, Type type)
|
||||
{
|
||||
if (type != null)
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Loaded type: {name}");
|
||||
else
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to load type: {name}");
|
||||
}
|
||||
|
||||
// --- Patching ---
|
||||
|
||||
private static void PatchAnalogControls()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_vector2ControlType != null)
|
||||
{
|
||||
MethodInfo vector2ReadValueMethod = _vector2ControlType.GetMethod("ReadValue",
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly,
|
||||
null, Type.EmptyTypes, null);
|
||||
|
||||
// If not found with DeclaredOnly, try getting from base type
|
||||
if (vector2ReadValueMethod == null)
|
||||
{
|
||||
Type baseType = _vector2ControlType.BaseType;
|
||||
if (baseType != null && baseType.IsGenericType)
|
||||
{
|
||||
vector2ReadValueMethod = baseType.GetMethod("ReadValue",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null, Type.EmptyTypes, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (vector2ReadValueMethod != null)
|
||||
{
|
||||
PatchMethod(vector2ReadValueMethod, null, nameof(Postfix_Vector2ReadValue));
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Patched Vector2Control.ReadValue()");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (_axisControlType != null)
|
||||
{
|
||||
MethodInfo readValueMethod = _axisControlType.GetMethod("ReadValue",
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly,
|
||||
null, Type.EmptyTypes, null);
|
||||
|
||||
// If not found with DeclaredOnly, try getting from base type
|
||||
if (readValueMethod == null)
|
||||
{
|
||||
Type baseType = _axisControlType.BaseType;
|
||||
if (baseType != null && baseType.IsGenericType)
|
||||
{
|
||||
readValueMethod = baseType.GetMethod("ReadValue",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null, Type.EmptyTypes, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (readValueMethod != null)
|
||||
{
|
||||
PatchMethod(readValueMethod, null, nameof(Postfix_AxisReadValue));
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Patched AxisControl.ReadValue()");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error during patching: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PatchMethod(MethodInfo method, string prefixName, string postfixName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (method == null)
|
||||
return;
|
||||
|
||||
#if CPP
|
||||
if (IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(method) == null)
|
||||
return;
|
||||
#endif
|
||||
|
||||
MethodInfo prefixMethod = prefixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), prefixName) : null;
|
||||
MethodInfo postfixMethod = postfixName != null ? AccessTools.Method(typeof(IGamepadInputInterceptor), postfixName) : null;
|
||||
|
||||
ExplorerCore.Harmony.Patch(method,
|
||||
prefix: prefixMethod != null ? new HarmonyMethod(prefixMethod) : null,
|
||||
postfix: postfixMethod != null ? new HarmonyMethod(postfixMethod) : null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to patch method: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Control Discovery ---
|
||||
|
||||
private static void DiscoverGamepadControls()
|
||||
{
|
||||
try
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] Discovering gamepad controls...");
|
||||
#endif
|
||||
|
||||
object gamepadInstance = GetGamepadInstance(_targetGamepadIndex);
|
||||
if (gamepadInstance == null)
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning("[IGamepadInputInterceptor] No gamepad found at specified index");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Instead of iterating allControls and trying to cast them,
|
||||
// directly access the typed control properties on the Gamepad class.
|
||||
// This gives us the controls with their correct types immediately.
|
||||
|
||||
// List of known public control properties on Gamepad
|
||||
string[] buttonProperties = new[]
|
||||
{
|
||||
"buttonSouth", "buttonNorth", "buttonWest", "buttonEast",
|
||||
"leftStickButton", "rightStickButton", "leftShoulder", "rightShoulder",
|
||||
"startButton", "selectButton", "leftTrigger", "rightTrigger"
|
||||
};
|
||||
|
||||
string[] vector2Properties = new[] { "leftStick", "rightStick" };
|
||||
|
||||
// Clear any existing gamepad buttons from INewInputSystem before registering new ones
|
||||
// This prevents duplicates when switching gamepads or re-initializing
|
||||
//var keysToRemove = INewInputSystem.buttonControls.Keys.Where(k => k.StartsWith("/gamepad") || k.Contains("gamepad")).ToList();
|
||||
//foreach (var key in keysToRemove)
|
||||
//{
|
||||
// INewInputSystem.buttonControls.Remove(key);
|
||||
//}
|
||||
|
||||
// Register all button controls
|
||||
foreach (string propName in buttonProperties)
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null)
|
||||
{
|
||||
object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (control != null)
|
||||
{
|
||||
string normalizedPath = ExtractAndNormalizePath(control);
|
||||
if (!string.IsNullOrEmpty(normalizedPath))
|
||||
{
|
||||
INewInputSystem.gamepadButtonControls[normalizedPath] = control;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered button: {normalizedPath} ({propName})");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access button {propName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Register D-pad individual direction buttons
|
||||
try
|
||||
{
|
||||
PropertyInfo dpadProp = _gamepadType.GetProperty("dpad", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (dpadProp != null)
|
||||
{
|
||||
object dpadControl = dpadProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (dpadControl != null)
|
||||
{
|
||||
string[] dpadDirections = new[] { "up", "down", "left", "right" };
|
||||
foreach (string direction in dpadDirections)
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo dpadDirProp = dpadControl.GetType().GetProperty(direction, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (dpadDirProp != null)
|
||||
{
|
||||
object buttonControl = dpadDirProp.GetValue(dpadControl, BindingFlags.Default, null, null, null);
|
||||
if (buttonControl != null)
|
||||
{
|
||||
string buttonPath = ExtractAndNormalizePath(buttonControl);
|
||||
if (!string.IsNullOrEmpty(buttonPath))
|
||||
{
|
||||
INewInputSystem.gamepadButtonControls[buttonPath] = buttonControl;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered dpad button: {buttonPath} (dpad.{direction})");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access dpad.{direction}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access dpad: {ex.Message}");
|
||||
}
|
||||
|
||||
// Register all vector2 controls (sticks)
|
||||
foreach (string propName in vector2Properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null)
|
||||
{
|
||||
object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (control != null)
|
||||
{
|
||||
string normalizedPath = ExtractAndNormalizePath(control);
|
||||
if (!string.IsNullOrEmpty(normalizedPath))
|
||||
{
|
||||
_vector2Controls[normalizedPath] = control;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered vector2: {normalizedPath} ({propName})");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Also register individual stick axes as axis controls
|
||||
// Use the stick name to differentiate between left and right sticks
|
||||
string[] axisNames = new[] { "x", "y" };
|
||||
foreach (string axisName in axisNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo axisProp = control.GetType().GetProperty(axisName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (axisProp != null)
|
||||
{
|
||||
object axisControl = axisProp.GetValue(control, BindingFlags.Default, null, null, null);
|
||||
if (axisControl != null)
|
||||
{
|
||||
string rawAxisPath = ExtractAndNormalizePath(axisControl);
|
||||
if (!string.IsNullOrEmpty(rawAxisPath))
|
||||
{
|
||||
// Inject the stick name into the path: /gamepad/x -> /gamepad/leftstick/x
|
||||
string stickName = propName.ToLowerInvariant(); // "leftstick" or "rightstick"
|
||||
string axisPath = $"/gamepad/{stickName}/{axisName}";
|
||||
_axisControls[axisPath] = axisControl;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered stick axis: {axisPath} ({propName}.{axisName})");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access {propName}.{axisName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access vector2 {propName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Register triggers as AXIS controls so analog values can be captured
|
||||
// Triggers are ButtonControl but also support analog values
|
||||
string[] triggerProperties = new[] { "leftTrigger", "rightTrigger" };
|
||||
foreach (string propName in triggerProperties)
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo prop = _gamepadType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (prop != null)
|
||||
{
|
||||
object control = prop.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (control != null)
|
||||
{
|
||||
string normalizedPath = ExtractAndNormalizePath(control);
|
||||
if (!string.IsNullOrEmpty(normalizedPath))
|
||||
{
|
||||
// Add trigger axis path (e.g., /gamepad/lefttrigger for analog value)
|
||||
_axisControls[normalizedPath] = control;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered trigger axis: {normalizedPath} ({propName})");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Failed to access {propName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
int totalControls = INewInputSystem.gamepadButtonControls.Count + _axisControls.Count + _vector2Controls.Count;
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Registered {totalControls} controls " +
|
||||
$"({INewInputSystem.gamepadButtonControls.Count} buttons, {_axisControls.Count} axes, {_vector2Controls.Count} vectors)");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error discovering controls: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetGamepadInstance(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to manually select the controller by index from the all collection
|
||||
if (_gamepadAllProp != null)
|
||||
{
|
||||
object allGamepads = _gamepadAllProp.GetValue(null, BindingFlags.Default, null, null, null);
|
||||
if (allGamepads != null)
|
||||
{
|
||||
Type collectionType = allGamepads.GetType();
|
||||
PropertyInfo countProp = collectionType.GetProperty("Count", BindingFlags.Public | BindingFlags.Instance);
|
||||
PropertyInfo itemProp = collectionType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
if (countProp != null && itemProp != null)
|
||||
{
|
||||
int count = (int)countProp.GetValue(allGamepads, BindingFlags.Default, null, null, null);
|
||||
if (index >= 0 && index < count)
|
||||
{
|
||||
return itemProp.GetValue(allGamepads, new object[] { index });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Gamepad.current
|
||||
if (_gamepadCurrentProp != null)
|
||||
{
|
||||
return _gamepadCurrentProp.GetValue(null, BindingFlags.Default, null, null, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error getting gamepad instance: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Some builds have problems when trying to retrieve property names, so we have to get some fallbacks into place while also trying to get a unified format.
|
||||
private static string ExtractAndNormalizePath(object control)
|
||||
{
|
||||
if (control == null)
|
||||
return null;
|
||||
|
||||
string path = null;
|
||||
|
||||
// Method 1: Try ToString() - safest on IL2CPP
|
||||
try
|
||||
{
|
||||
string toString = control.ToString();
|
||||
if (!string.IsNullOrEmpty(toString))
|
||||
{
|
||||
// Parse path from ToString (e.g., "ButtonControl{/xinputcontrollerwindows/buttonSouth}")
|
||||
int slashIndex = toString.IndexOf('/');
|
||||
if (slashIndex >= 0)
|
||||
{
|
||||
int closeIndex = toString.IndexOf('}', slashIndex);
|
||||
if (closeIndex >= 0)
|
||||
{
|
||||
path = toString.Substring(slashIndex, closeIndex - slashIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Method 2: Try reflection on path property (managed builds)
|
||||
#if CPP
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_basePathProp != null)
|
||||
{
|
||||
object pathObj = _basePathProp.GetValue(control, BindingFlags.Default, null, null, null);
|
||||
path = pathObj as string;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
#endif
|
||||
|
||||
// Method 3: Try via name property (fallback)
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
PropertyInfo nameProp = control.GetType().GetProperty("name", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (nameProp != null)
|
||||
{
|
||||
string controlName = nameProp.GetValue(control, BindingFlags.Default, null, null, null) as string;
|
||||
if (!string.IsNullOrEmpty(controlName))
|
||||
{
|
||||
path = $"/gamepad/{controlName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
return NormalizeControlPath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes gamepad control paths to a common format across different controller types.
|
||||
/// </summary>
|
||||
public static string NormalizeControlPath(string rawPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawPath))
|
||||
return rawPath;
|
||||
|
||||
string path = rawPath.ToLowerInvariant().Trim();
|
||||
|
||||
foreach (var kvp in _vendorPrefixMap)
|
||||
{
|
||||
if (path.StartsWith(kvp.Key))
|
||||
{
|
||||
path = kvp.Value + path.Substring(kvp.Key.Length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove numeric gamepad suffix (/gamepad1 → /gamepad)
|
||||
const string prefix = "/gamepad";
|
||||
int prefixLen = prefix.Length;
|
||||
if (path.StartsWith(prefix) &&
|
||||
path.Length > prefixLen &&
|
||||
char.IsDigit(path[prefixLen]))
|
||||
{
|
||||
path = prefix + path.Substring(prefixLen + 1);
|
||||
}
|
||||
|
||||
// Normalize D-pad paths: /gamepad/up -> /gamepad/dpad/up
|
||||
// Some versions of the engine/library return these names
|
||||
if (path == "/gamepad/up" || path == "/gamepad/down" ||
|
||||
path == "/gamepad/left" || path == "/gamepad/right")
|
||||
{
|
||||
string direction = path.Substring("/gamepad/".Length);
|
||||
path = $"/gamepad/dpad/{direction}";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void ClearControlCaches()
|
||||
{
|
||||
INewInputSystem.gamepadButtonControls.Clear();
|
||||
_axisControls.Clear();
|
||||
_vector2Controls.Clear();
|
||||
_axisValues.Clear();
|
||||
_vector2Values.Clear();
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a button is currently pressed.
|
||||
/// </summary>
|
||||
/// <param name="buttonPath">Normalized path like "/gamepad/buttonsouth" or vendor-specific path</param>
|
||||
public static bool IsButtonPressed(string buttonPath)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return false;
|
||||
|
||||
string key = buttonPath.ToLower();
|
||||
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed querying: {key}");
|
||||
#endif
|
||||
|
||||
// Trigger the property getter to populate state via INewInputSystem
|
||||
if (INewInputSystem.gamepadButtonControls.TryGetValue(key, out object button) && _buttonIsPressedProp != null)
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed found button, invoking getter");
|
||||
#endif
|
||||
_buttonIsPressedProp.GetValue(button, BindingFlags.Default, null, null, null);
|
||||
}
|
||||
#if IGAMEPAD_DEBUG
|
||||
else
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed NOT found in gamepadButtonControls. Available: {string.Join(", ", INewInputSystem.gamepadButtonControls.Keys)}");
|
||||
}
|
||||
#endif
|
||||
bool result = INewInputSystem.buttonPressedStates.TryGetValue(key, out bool pressed) && pressed;
|
||||
#if IGAMEPAD_DEBUG
|
||||
//ExplorerCore.LogWarning($"[IGamepadInputInterceptor] IsButtonPressed returning: {result}");
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a button was pressed this frame.
|
||||
/// </summary>
|
||||
public static bool WasButtonPressedThisFrame(string buttonPath)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return false;
|
||||
|
||||
string key = buttonPath.ToLower();
|
||||
|
||||
// Trigger the property getter to populate state via INewInputSystem
|
||||
if (INewInputSystem.gamepadButtonControls.TryGetValue(key, out object button) && _buttonWasPressedProp != null)
|
||||
{
|
||||
_buttonWasPressedProp.GetValue(button, BindingFlags.Default, null, null, null);
|
||||
}
|
||||
|
||||
return INewInputSystem.buttonWasPressedStates.TryGetValue(key, out bool pressed) && pressed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a button was released this frame.
|
||||
/// </summary>
|
||||
public static bool WasButtonReleasedThisFrame(string buttonPath)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return false;
|
||||
|
||||
string key = buttonPath.ToLower();
|
||||
|
||||
// Trigger the property getter to populate state via INewInputSystem
|
||||
if (INewInputSystem.gamepadButtonControls.TryGetValue(key, out object button) && _buttonWasReleasedProp != null)
|
||||
{
|
||||
_buttonWasReleasedProp.GetValue(button, BindingFlags.Default, null, null, null);
|
||||
}
|
||||
|
||||
return INewInputSystem.buttonWasReleasedStates.TryGetValue(key, out bool released) && released;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of an analog axis (trigger, stick axis, etc).
|
||||
/// </summary>
|
||||
/// <param name="axisPath">Path like "/gamepad/lefttrigger" or "/gamepad/leftstick/x"</param>
|
||||
public static float GetAxisValue(string axisPath)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return 0f;
|
||||
|
||||
string normalized = NormalizeControlPath(axisPath ?? "");
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue querying: {normalized}");
|
||||
#endif
|
||||
|
||||
// Trigger the ReadValue() method to populate state
|
||||
if (_axisControls.TryGetValue(normalized, out object axis) && _axisReadValueMethod != null)
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue found axis, invoking ReadValue()");
|
||||
#endif
|
||||
_axisReadValueMethod.Invoke(axis, null);
|
||||
}
|
||||
#if IGAMEPAD_DEBUG
|
||||
else
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] GetAxisValue NOT found for: {normalized}. Available: {string.Join(", ", _axisControls.Keys)}");
|
||||
}
|
||||
#endif
|
||||
return _axisValues.TryGetValue(normalized, out float value) ? value : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of a 2D input (stick, dpad).
|
||||
/// </summary>
|
||||
/// <param name="vectorPath">Path like "/gamepad/leftstick" or "/gamepad/dpad"</param>
|
||||
public static Vector2 GetVector2Value(string vectorPath)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return new Vector2(0f, 0f);
|
||||
|
||||
string normalized = NormalizeControlPath(vectorPath ?? "");
|
||||
|
||||
// Trigger the ReadValue() method to populate state
|
||||
if (_vector2Controls.TryGetValue(normalized, out object vector2) && _vector2ReadValueMethod != null)
|
||||
{
|
||||
_vector2ReadValueMethod.Invoke(vector2, null);
|
||||
}
|
||||
|
||||
return _vector2Values.TryGetValue(normalized, out var value) ? value : new Vector2(0f, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all currently registered control paths.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetRegisteredButtonPaths() => INewInputSystem.gamepadButtonControls.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all currently registered axis paths.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetRegisteredAxisPaths() => _axisControls.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all currently registered vector2 paths.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetRegisteredVector2Paths() => _vector2Controls.Keys;
|
||||
|
||||
// --- Postfix Methods (capture state and optionally block) ---
|
||||
|
||||
private static void Postfix_Vector2ReadValue(object __instance, ref object __result)
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_Vector2ReadValue called, instance={(__instance != null ? "not null" : "null")}, result={(__result != null ? "not null" : "null")}");
|
||||
#endif
|
||||
if (__instance == null || __result == null)
|
||||
return;
|
||||
|
||||
// Verify the vector2 control is properly attached to a device before proceeding
|
||||
try
|
||||
{
|
||||
if (_deviceProp != null)
|
||||
{
|
||||
object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null);
|
||||
if (device == null)
|
||||
return; // Control not attached to a device, skip
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return; // If we can't check device, skip to be safe
|
||||
}
|
||||
|
||||
string path = ExtractAndNormalizePath(__instance);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_vector2XProp != null && _vector2YProp != null)
|
||||
{
|
||||
float x = (float)_vector2XProp.GetValue(__result, BindingFlags.Default, null, null, null);
|
||||
float y = (float)_vector2YProp.GetValue(__result, BindingFlags.Default, null, null, null);
|
||||
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_Vector2ReadValue: {path} = ({x}, {y})");
|
||||
#endif
|
||||
_vector2Values[path] = new Vector2(x, y);
|
||||
|
||||
if (_isEnabled && FreeCamPanel.ShouldOverrideInput() && _vector2XProp.CanWrite && _vector2YProp.CanWrite)
|
||||
{
|
||||
_vector2XProp.SetValue(__result, 0f, BindingFlags.Default, null, null, null);
|
||||
_vector2YProp.SetValue(__result, 0f, BindingFlags.Default, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
#if IGAMEPAD_DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error in Postfix_Vector2ReadValue: {ex.Message}");
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static void Postfix_AxisReadValue(object __instance, ref float __result)
|
||||
{
|
||||
if (__instance == null)
|
||||
return;
|
||||
|
||||
// Verify the axis control is properly attached to a device before proceeding
|
||||
try
|
||||
{
|
||||
if (_deviceProp != null)
|
||||
{
|
||||
object device = _deviceProp.GetValue(__instance, BindingFlags.Default, null, null, null);
|
||||
if (device == null)
|
||||
return; // Control not attached to a device, skip
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return; // If we can't check device, skip to be safe
|
||||
}
|
||||
|
||||
string path = ExtractAndNormalizePath(__instance);
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_AxisReadValue extracted path: {path}, value: {__result}");
|
||||
#endif
|
||||
|
||||
// Try to identify if this axis belongs to a stick and which one by comparing the parent stick's axes
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
string adjustedPath = path;
|
||||
|
||||
try
|
||||
{
|
||||
// Get the gamepad instance to access stick controls
|
||||
object gamepadInstance = GetGamepadInstance(_targetGamepadIndex);
|
||||
if (gamepadInstance != null)
|
||||
{
|
||||
// Check if this axis is part of leftStick
|
||||
if (_gamepadLeftStickProp != null)
|
||||
{
|
||||
object leftStick = _gamepadLeftStickProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (leftStick != null)
|
||||
{
|
||||
PropertyInfo leftStickXProp = leftStick.GetType().GetProperty("x", BindingFlags.Public | BindingFlags.Instance);
|
||||
PropertyInfo leftStickYProp = leftStick.GetType().GetProperty("y", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
object leftStickX = leftStickXProp?.GetValue(leftStick, BindingFlags.Default, null, null, null);
|
||||
object leftStickY = leftStickYProp?.GetValue(leftStick, BindingFlags.Default, null, null, null);
|
||||
|
||||
// Compare by reference to see if __instance is the x or y of leftStick
|
||||
if (__instance == leftStickX || __instance == leftStickY)
|
||||
{
|
||||
adjustedPath = path.Replace("/gamepad/x", "/gamepad/leftstick/x").Replace("/gamepad/y", "/gamepad/leftstick/y");
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Identified as LEFT STICK axis, adjusted path: {adjustedPath}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not left stick, check if it's part of rightStick
|
||||
if (adjustedPath == path && _gamepadRightStickProp != null)
|
||||
{
|
||||
object rightStick = _gamepadRightStickProp.GetValue(gamepadInstance, BindingFlags.Default, null, null, null);
|
||||
if (rightStick != null)
|
||||
{
|
||||
PropertyInfo rightStickXProp = rightStick.GetType().GetProperty("x", BindingFlags.Public | BindingFlags.Instance);
|
||||
PropertyInfo rightStickYProp = rightStick.GetType().GetProperty("y", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
object rightStickX = rightStickXProp?.GetValue(rightStick, BindingFlags.Default, null, null, null);
|
||||
object rightStickY = rightStickYProp?.GetValue(rightStick, BindingFlags.Default, null, null, null);
|
||||
|
||||
// Compare by reference to see if __instance is the x or y of rightStick
|
||||
if (__instance == rightStickX || __instance == rightStickY)
|
||||
{
|
||||
adjustedPath = path.Replace("/gamepad/x", "/gamepad/rightstick/x").Replace("/gamepad/y", "/gamepad/rightstick/y");
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Identified as RIGHT STICK axis, adjusted path: {adjustedPath}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Error identifying stick axis: {ex.Message}");
|
||||
}
|
||||
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] Postfix_AxisReadValue storing: {adjustedPath} = {__result}");
|
||||
#endif
|
||||
_axisValues[adjustedPath] = __result;
|
||||
// Block input if interception is enabled
|
||||
if (_isEnabled && FreeCamPanel.ShouldOverrideInput())
|
||||
{
|
||||
#if IGAMEPAD_DEBUG
|
||||
ExplorerCore.LogWarning($"[IGamepadInputInterceptor] BLOCKING Postfix_AxisReadValue: {adjustedPath} (was {__result}, now 0f)");
|
||||
#endif
|
||||
__result = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ namespace UniverseLib.Input
|
||||
ILegacyInput.Init();
|
||||
break;
|
||||
case InputType.InputSystem:
|
||||
INewInputSystem.Init();
|
||||
break;
|
||||
case InputType.None:
|
||||
break;
|
||||
}
|
||||
@@ -38,6 +40,7 @@ namespace UniverseLib.Input
|
||||
case InputType.Legacy:
|
||||
return ILegacyInput.GetKey(key);
|
||||
case InputType.InputSystem:
|
||||
return INewInputSystem.GetKey(key);
|
||||
case InputType.None:
|
||||
default:
|
||||
return InputManager.GetKey(key);
|
||||
@@ -49,6 +52,8 @@ namespace UniverseLib.Input
|
||||
case InputType.Legacy:
|
||||
return ILegacyInput.GetKeyDown(key);
|
||||
case InputType.InputSystem:
|
||||
string buttonName = $"/keyboard/{key.ToString()}".ToLower();
|
||||
return INewInputSystem.GetButtonWasPressed(buttonName);
|
||||
case InputType.None:
|
||||
default:
|
||||
return InputManager.GetKeyDown(key);
|
||||
@@ -60,6 +65,9 @@ namespace UniverseLib.Input
|
||||
case InputType.Legacy:
|
||||
return ILegacyInput.GetKeyUp(key);
|
||||
case InputType.InputSystem:
|
||||
// Closest equivalent in the new input system is "wasPressedThisFrame"?
|
||||
string buttonName = $"/keyboard/{key.ToString()}".ToLower();
|
||||
return INewInputSystem.GetButtonWasReleased(buttonName);
|
||||
case InputType.None:
|
||||
default:
|
||||
return InputManager.GetKeyUp(key);
|
||||
@@ -71,6 +79,7 @@ namespace UniverseLib.Input
|
||||
case InputType.Legacy:
|
||||
return ILegacyInput.GetMouseButton(button);
|
||||
case InputType.InputSystem:
|
||||
return INewInputSystem.GetMouseButton(button);
|
||||
case InputType.None:
|
||||
default:
|
||||
return InputManager.GetMouseButton(button);
|
||||
@@ -82,6 +91,7 @@ namespace UniverseLib.Input
|
||||
case InputType.Legacy:
|
||||
return ILegacyInput.GetMouseButtonDown(button);
|
||||
case InputType.InputSystem:
|
||||
return INewInputSystem.GetMouseButtonDown(button);
|
||||
case InputType.None:
|
||||
default:
|
||||
return InputManager.GetMouseButtonDown(button);
|
||||
@@ -325,4 +335,289 @@ namespace UniverseLib.Input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class INewInputSystem
|
||||
{
|
||||
// --- Reflected members ---
|
||||
private static PropertyInfo isPressedProp;
|
||||
private static PropertyInfo wasPressedProp;
|
||||
private static PropertyInfo wasReleasedProp;
|
||||
private static MethodInfo isValueConsideredPressed;
|
||||
private static MethodInfo isPressedMethod;
|
||||
private static MethodInfo isInProgressMethod;
|
||||
private static MethodInfo wasPressedMethod;
|
||||
private static MethodInfo wasPerformedMethod;
|
||||
|
||||
// --- State dictionaries ---
|
||||
internal static readonly Dictionary<string, object> buttonControls = new();
|
||||
internal static readonly Dictionary<string, object> gamepadButtonControls = new();
|
||||
internal static readonly Dictionary<string, bool> buttonPressedStates = new();
|
||||
internal static readonly Dictionary<string, bool> buttonWasPressedStates = new();
|
||||
internal static readonly Dictionary<string, bool> buttonWasReleasedStates = new();
|
||||
private static readonly Dictionary<string, bool> buttonIsValueConsideredPressedStates = new();
|
||||
private static readonly Dictionary<string, bool> actionInProgressStates = new();
|
||||
private static readonly Dictionary<string, bool> actionWasPerformedStates = new();
|
||||
|
||||
// --- Initialization ---
|
||||
public static void Init()
|
||||
{
|
||||
Type buttonControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.ButtonControl, Unity.InputSystem");
|
||||
Type inputActionType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.InputAction, Unity.InputSystem");
|
||||
|
||||
if (buttonControlType == null || inputActionType == null)
|
||||
{
|
||||
ExplorerCore.LogWarning("Unity.InputSystem types not found; InputSystem integration disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
// === Patch all relevant ButtonControl properties ===
|
||||
PatchButtonControl(buttonControlType);
|
||||
|
||||
// === Patch InputAction methods ===
|
||||
PatchInputAction(inputActionType);
|
||||
|
||||
// === Cache controls ===
|
||||
GetButtonControls();
|
||||
|
||||
IGamepadInputInterceptor.Init();
|
||||
}
|
||||
|
||||
private static void PatchButtonControl(Type buttonControlType)
|
||||
{
|
||||
isPressedProp = buttonControlType.GetProperty("isPressed");
|
||||
wasPressedProp = buttonControlType.GetProperty("wasPressedThisFrame");
|
||||
wasReleasedProp = buttonControlType.GetProperty("wasReleasedThisFrame");
|
||||
isValueConsideredPressed = buttonControlType.GetMethod("IsValueConsideredPressed");
|
||||
|
||||
PatchPropertyGetter(isPressedProp, nameof(Postfix_IsPressed));
|
||||
PatchPropertyGetter(wasPressedProp, nameof(Postfix_WasPressedThisFrame));
|
||||
PatchPropertyGetter(wasReleasedProp, nameof(Postfix_WasReleasedThisFrame));
|
||||
PatchPropertyGetter(wasReleasedProp, nameof(Postfix_IsValueConsideredPressed));
|
||||
}
|
||||
|
||||
private static void PatchInputAction(Type inputActionType)
|
||||
{
|
||||
isPressedMethod = inputActionType.GetMethod("IsPressed");
|
||||
isInProgressMethod = inputActionType.GetMethod("IsInProgress");
|
||||
wasPressedMethod = inputActionType.GetMethod("WasPressedThisFrame");
|
||||
wasPerformedMethod = inputActionType.GetMethod("WasPerformedThisFrame");
|
||||
|
||||
PatchMethod(isPressedMethod, nameof(Postfix_IsPressed));
|
||||
PatchMethod(isInProgressMethod, nameof(Postfix_IsInProgress));
|
||||
PatchMethod(wasPressedMethod, nameof(Postfix_WasPressedThisFrame));
|
||||
PatchMethod(wasPerformedMethod, nameof(Postfix_WasPerformedThisFrame));
|
||||
}
|
||||
|
||||
private static void PatchPropertyGetter(PropertyInfo prop, string postfixName)
|
||||
{
|
||||
if (prop?.GetGetMethod() == null) return;
|
||||
#if CPP
|
||||
if (IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(prop.GetGetMethod()) == null)
|
||||
throw new Exception();
|
||||
#endif
|
||||
ExplorerCore.Harmony.Patch(prop.GetGetMethod(),
|
||||
postfix: new HarmonyMethod(AccessTools.Method(typeof(INewInputSystem), postfixName)));
|
||||
}
|
||||
|
||||
private static void PatchMethod(MethodInfo method, string postfixName)
|
||||
{
|
||||
if (method == null) return;
|
||||
#if CPP
|
||||
if (IL2CPPUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(method) == null)
|
||||
throw new Exception();
|
||||
#endif
|
||||
ExplorerCore.Harmony.Patch(method,
|
||||
postfix: new HarmonyMethod(AccessTools.Method(typeof(INewInputSystem), postfixName)));
|
||||
}
|
||||
|
||||
// --- Cache button controls ---
|
||||
public static void GetButtonControls()
|
||||
{
|
||||
Type buttonControlType = ReflectionUtility.GetTypeByName("UnityEngine.InputSystem.Controls.ButtonControl, Unity.InputSystem");
|
||||
PropertyInfo pathProp = buttonControlType?.GetProperty("path", BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
RegisterDeviceControls("UnityEngine.InputSystem.Keyboard", "UnityEngine.InputSystem.Controls.KeyControl", pathProp);
|
||||
RegisterDeviceControls("UnityEngine.InputSystem.Mouse", "UnityEngine.InputSystem.Controls.ButtonControl", pathProp);
|
||||
// Note: Gamepad buttons are registered by IGamepadInputInterceptor with proper path normalization
|
||||
}
|
||||
|
||||
internal static void RegisterDeviceControls(string deviceTypeName, string controlTypeName, PropertyInfo pathProp)
|
||||
{
|
||||
Type deviceType = ReflectionUtility.GetTypeByName($"{deviceTypeName}, Unity.InputSystem");
|
||||
if (deviceType == null) return;
|
||||
|
||||
PropertyInfo currentProp = deviceType.GetProperty("current", BindingFlags.Public | BindingFlags.Static);
|
||||
object deviceInstance = currentProp?.GetValue(null, null);
|
||||
if (deviceInstance == null) return;
|
||||
|
||||
foreach (PropertyInfo prop in deviceType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (prop.GetIndexParameters().Length > 0)
|
||||
continue;
|
||||
|
||||
if (prop.PropertyType.FullName == controlTypeName)
|
||||
{
|
||||
object control = prop.GetValue(deviceInstance, null);
|
||||
if (control == null)
|
||||
continue;
|
||||
|
||||
string path = pathProp?.GetValue(control, null) as string;
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
buttonControls[path.ToLower()] = control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Access methods ---
|
||||
|
||||
public static bool GetKey(KeyCode key)
|
||||
{
|
||||
string buttonName = PropToKeycode($"/keyboard/{key.ToString()}".ToLower());
|
||||
return GetButtonPressed(buttonName);
|
||||
}
|
||||
|
||||
public static bool GetButtonPressed(string buttonName)
|
||||
{
|
||||
string key = buttonName.ToLower();
|
||||
if (buttonControls.TryGetValue(key, out object button))
|
||||
{
|
||||
isPressedProp?.GetValue(button, null);
|
||||
return buttonPressedStates.TryGetValue(key, out bool value) && value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool GetButtonWasPressed(string buttonName)
|
||||
{
|
||||
string key = buttonName.ToLower();
|
||||
if (buttonControls.TryGetValue(key, out object button))
|
||||
{
|
||||
wasPressedProp?.GetValue(button, null);
|
||||
return buttonWasPressedStates.TryGetValue(key, out bool value) && value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool GetButtonWasReleased(string buttonName)
|
||||
{
|
||||
string key = buttonName.ToLower();
|
||||
if (buttonControls.TryGetValue(key, out object button))
|
||||
{
|
||||
wasReleasedProp?.GetValue(button, null);
|
||||
return buttonWasReleasedStates.TryGetValue(key, out bool value) && value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool GetMouseButton(int button)
|
||||
{
|
||||
string buttonPath = button switch
|
||||
{
|
||||
0 => "/mouse/leftbutton",
|
||||
1 => "/mouse/rightbutton",
|
||||
2 => "/mouse/middlebutton",
|
||||
3 => "/mouse/forwardbutton",
|
||||
4 => "/mouse/backbutton",
|
||||
_ => $"/mouse/button{button}"
|
||||
};
|
||||
|
||||
return GetButtonPressed(buttonPath);
|
||||
}
|
||||
|
||||
public static bool GetMouseButtonDown(int button){
|
||||
string buttonPath = button switch
|
||||
{
|
||||
0 => "/mouse/leftbutton",
|
||||
1 => "/mouse/rightbutton",
|
||||
2 => "/mouse/middlebutton",
|
||||
3 => "/mouse/forwardbutton",
|
||||
4 => "/mouse/backbutton",
|
||||
_ => $"/mouse/button{button}"
|
||||
};
|
||||
return GetButtonWasPressed(buttonPath);
|
||||
}
|
||||
|
||||
// --- Postfixes ---
|
||||
|
||||
private static void Postfix_IsPressed(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, buttonPressedStates);
|
||||
|
||||
private static void Postfix_WasPressedThisFrame(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, buttonWasPressedStates);
|
||||
|
||||
private static void Postfix_WasReleasedThisFrame(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, buttonWasReleasedStates);
|
||||
|
||||
private static void Postfix_IsValueConsideredPressed(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, buttonIsValueConsideredPressedStates);
|
||||
|
||||
private static void Postfix_IsInProgress(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, actionInProgressStates);
|
||||
|
||||
private static void Postfix_WasPerformedThisFrame(object __instance, ref bool __result)
|
||||
=> StoreState(__instance, ref __result, actionWasPerformedStates);
|
||||
|
||||
private static void StoreState(object __instance, ref bool __result, Dictionary<string, bool> dict)
|
||||
{
|
||||
try
|
||||
{
|
||||
Type type = __instance.GetType();
|
||||
PropertyInfo pathProp = type.GetProperty("path") ?? type.GetProperty("name");
|
||||
string key = (pathProp?.GetValue(__instance, null)?.ToString() ?? string.Empty).ToLower();
|
||||
|
||||
// Normalize gamepad paths so they match what IGamepadInputInterceptor registered
|
||||
key = IGamepadInputInterceptor.NormalizeControlPath(key);
|
||||
|
||||
dict[key] = __result;
|
||||
|
||||
if (FreeCamPanel.ShouldOverrideInput())
|
||||
{
|
||||
__result = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExplorerCore.LogWarning($"Failed to store state: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Input system property name doesnt match the KeyCode enum name, so we need to map some
|
||||
public static string PropToKeycode(string propName)
|
||||
{
|
||||
return propName switch
|
||||
{
|
||||
"/keyboard/leftcontrol" => "/keyboard/leftctrl",
|
||||
"/keyboard/rightcontrol" => "/keyboard/rightctrl",
|
||||
"/keyboard/keypad0" => "/keyboard/numpad0",
|
||||
"/keyboard/keypad1" => "/keyboard/numpad1",
|
||||
"/keyboard/keypad2" => "/keyboard/numpad2",
|
||||
"/keyboard/keypad3" => "/keyboard/numpad3",
|
||||
"/keyboard/keypad4" => "/keyboard/numpad4",
|
||||
"/keyboard/keypad5" => "/keyboard/numpad5",
|
||||
"/keyboard/keypad6" => "/keyboard/numpad6",
|
||||
"/keyboard/keypad7" => "/keyboard/numpad7",
|
||||
"/keyboard/keypad8" => "/keyboard/numpad8",
|
||||
"/keyboard/keypad9" => "/keyboard/numpad9",
|
||||
"/keyboard/keypaddivide" => "/keyboard/numpaddivide",
|
||||
"/keyboard/keypadmultiply" => "/keyboard/numpadmultiply",
|
||||
"/keyboard/keypadminus" => "/keyboard/numpadminus",
|
||||
"/keyboard/keypadplus" => "/keyboard/numpadplus",
|
||||
"/keyboard/keypadperiod" => "/keyboard/numpadperiod",
|
||||
"/keyboard/keypadequals" => "/keyboard/numpadequals",
|
||||
"/keyboard/keypadenter" => "/keyboard/numpadenter",
|
||||
"/keyboard/alpha0" => "/keyboard/0",
|
||||
"/keyboard/alpha1" => "/keyboard/1",
|
||||
"/keyboard/alpha2" => "/keyboard/2",
|
||||
"/keyboard/alpha3" => "/keyboard/3",
|
||||
"/keyboard/alpha4" => "/keyboard/4",
|
||||
"/keyboard/alpha5" => "/keyboard/5",
|
||||
"/keyboard/alpha6" => "/keyboard/6",
|
||||
"/keyboard/alpha7" => "/keyboard/7",
|
||||
"/keyboard/alpha8" => "/keyboard/8",
|
||||
"/keyboard/alpha9" => "/keyboard/9",
|
||||
"/keyboard/print" => "/keyboard/printscreen",
|
||||
_ => propName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,10 @@ namespace UnityExplorer.Serializers
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public readonly Vector3 position;
|
||||
public readonly Vector3 angles;
|
||||
public readonly Vector3 scale;
|
||||
// They cant be read-only because it causes problems with XML serialization in older versions of .NET
|
||||
public Vector3 position { get; set; }
|
||||
public Vector3 angles { get; set; }
|
||||
public Vector3 scale { get; set; }
|
||||
|
||||
public void CopyToTransform(Transform transform){
|
||||
transform.localPosition = position;
|
||||
|
||||
@@ -11,6 +11,52 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
// StepCommand is basically the offset of step_left and step_up, what IGCS sends to move the camera.
|
||||
using StepCommand = Mono.CSharp.Tuple<float, float>;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct Vec3
|
||||
{
|
||||
public float x, y, z;
|
||||
|
||||
public void FromVector3(Vector3 v)
|
||||
{
|
||||
this.x = v.x; this.y = v.y; this.z = v.z;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct Vec4
|
||||
{
|
||||
public float x, y, z, w;
|
||||
public void FromQuaternion(Quaternion q) { this.x = q.x; this.y = q.y ; this.z = q.z; this.w = q.w; }
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct CameraToolsData
|
||||
{
|
||||
public byte cameraEnabled;
|
||||
public byte cameraMovementLocked;
|
||||
public byte reserved1;
|
||||
public byte reserved2;
|
||||
public float fov;
|
||||
public Vec3 coordinates;
|
||||
public Vec4 lookQuaternion;
|
||||
public Vec3 rotationMatrixUpVector;
|
||||
public Vec3 rotationMatrixRightVector;
|
||||
public Vec3 rotationMatrixForwardVector;
|
||||
public float pitch;
|
||||
public float yaw;
|
||||
public float roll;
|
||||
}
|
||||
|
||||
enum SessionStatus : int
|
||||
{
|
||||
Ok = 0,
|
||||
Error_CameraNotEnabled = 1,
|
||||
Error_CameraPathPlaying = 2,
|
||||
Error_AlreadySessionActive = 3,
|
||||
Error_CameraFeatureNotAvailable = 4,
|
||||
Error_UnknownError = 5
|
||||
}
|
||||
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("kernel32.dll")]
|
||||
@@ -26,7 +72,7 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
private delegate void MoveCameraCallback(float step_left, float step_up, float fov, int from_start);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SessionCallback();
|
||||
private delegate int SessionCallback();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate IntPtr U_IGCS_Initialize(MoveCameraCallback callback, SessionCallback start_cb, SessionCallback end_cb);
|
||||
@@ -49,13 +95,60 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
// This object *must* be used with a Lock.
|
||||
private readonly Queue<StepCommand> commands = new();
|
||||
|
||||
private IntPtr CameraStatus = IntPtr.Zero;
|
||||
private IntPtr AllCamerasData = IntPtr.Zero;
|
||||
|
||||
private IEnumerable<IntPtr> GetValidCameras()
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
IntPtr camPtr = Marshal.ReadIntPtr(AllCamerasData, i * IntPtr.Size);
|
||||
|
||||
if (camPtr == IntPtr.Zero)
|
||||
yield break;
|
||||
|
||||
yield return camPtr;
|
||||
}
|
||||
}
|
||||
public void UpdateFreecamStatus(bool enabled)
|
||||
{
|
||||
if (CameraStatus == IntPtr.Zero) return;
|
||||
|
||||
Marshal.WriteByte(CameraStatus, enabled ? (byte)0x1 : (byte)0x0);
|
||||
if (AllCamerasData == IntPtr.Zero) return;
|
||||
unsafe
|
||||
{
|
||||
foreach (IntPtr pCamera in this.GetValidCameras())
|
||||
{
|
||||
CameraToolsData* data = (CameraToolsData*)pCamera;
|
||||
data->cameraEnabled = enabled ? (byte)1 : (byte) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateCameraData(Camera cam)
|
||||
{
|
||||
if (AllCamerasData == IntPtr.Zero) return;
|
||||
|
||||
Transform transform = cam.transform;
|
||||
unsafe
|
||||
{
|
||||
foreach (IntPtr pCamera in this.GetValidCameras())
|
||||
{
|
||||
CameraToolsData* data = (CameraToolsData*)pCamera;
|
||||
data->fov = cam.fieldOfView;
|
||||
data->cameraMovementLocked = _isActive ? (byte)1 : (byte)0;
|
||||
|
||||
data->coordinates.FromVector3(transform.position);
|
||||
|
||||
data->lookQuaternion.FromQuaternion(transform.rotation);
|
||||
|
||||
data->rotationMatrixRightVector.FromVector3(transform.right);
|
||||
data->rotationMatrixUpVector.FromVector3(transform.up);
|
||||
data->rotationMatrixForwardVector.FromVector3(transform.forward);
|
||||
|
||||
Vector3 rot = transform.eulerAngles;
|
||||
data->pitch = rot.x * Mathf.Deg2Rad;
|
||||
data->yaw = rot.y * Mathf.Deg2Rad;
|
||||
data->roll = rot.z * Mathf.Deg2Rad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteCameraCommand(Camera cam)
|
||||
@@ -92,9 +185,11 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
commands.Enqueue(new StepCommand(stepLeft, stepUp));
|
||||
}
|
||||
}
|
||||
private void StartSession()
|
||||
private int StartSession()
|
||||
{
|
||||
if (_isActive) return (int)SessionStatus.Error_AlreadySessionActive;
|
||||
_isActive = true;
|
||||
return (int)SessionStatus.Ok;
|
||||
}
|
||||
|
||||
// At the EndSession, since we have a queue system, we have to have a special check when the session ends and
|
||||
@@ -111,7 +206,7 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
endSessionPosition = null;
|
||||
}
|
||||
|
||||
private void EndSession()
|
||||
private int EndSession()
|
||||
{
|
||||
endSessionPosition = position;
|
||||
position = null;
|
||||
@@ -119,6 +214,8 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
|
||||
lock (commands)
|
||||
commands.Clear();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public UnityIGCSConnector()
|
||||
@@ -143,8 +240,8 @@ namespace CinematicUnityExplorer.Cinematic
|
||||
delegates.Add(new SessionCallback(StartSession));
|
||||
delegates.Add(new SessionCallback(EndSession));
|
||||
|
||||
CameraStatus = initFunc((MoveCameraCallback)delegates[0], (SessionCallback)delegates[1], (SessionCallback)delegates[2]);
|
||||
if (CameraStatus == IntPtr.Zero){
|
||||
AllCamerasData = initFunc((MoveCameraCallback)delegates[0], (SessionCallback)delegates[1], (SessionCallback)delegates[2]);
|
||||
if (AllCamerasData == IntPtr.Zero){
|
||||
// This is actually a InvalidDataException, but some games dont allow you to throw that.
|
||||
throw new Exception("IGCSDof returned an invalid pointer which means something went wrong");
|
||||
}
|
||||
|
||||
+32
-10
@@ -32,8 +32,9 @@ namespace UnityExplorer.Config
|
||||
public static ConfigElement<bool> Reflection_Hide_NativeInfoPtrs;
|
||||
public static ConfigElement<bool> Auto_Scale_UI;
|
||||
public static ConfigElement<bool> Reset_Camera_Transform;
|
||||
public static ConfigElement<float> Arrow_Size;
|
||||
public static ConfigElement<bool> Freecam_Camera_Target_Selection;
|
||||
|
||||
public static ConfigElement<FreeCamPanel.FreeCameraType> Default_Freecam;
|
||||
public static ConfigElement<KeyCode> Pause;
|
||||
public static ConfigElement<KeyCode> Frameskip;
|
||||
public static ConfigElement<KeyCode> Screenshot;
|
||||
@@ -61,6 +62,10 @@ namespace UnityExplorer.Config
|
||||
public static ConfigElement<KeyCode> Reset_FOV;
|
||||
public static ConfigElement<KeyCode> Toggle_Animations;
|
||||
|
||||
public static ConfigElement<FreeCamPanel.FreeCameraType> Default_Freecam;
|
||||
public static ConfigElement<string> Custom_Components_To_Disable;
|
||||
public static ConfigElement<string> Preferred_Target_Camera;
|
||||
|
||||
// internal configs
|
||||
internal static InternalConfigHandler InternalHandler { get; private set; }
|
||||
internal static readonly Dictionary<UIManager.Panels, ConfigElement<string>> PanelSaveData = new();
|
||||
@@ -166,7 +171,7 @@ namespace UnityExplorer.Config
|
||||
"Optional keybind to begin a UI-mode Mouse Inspect.",
|
||||
KeyCode.None);
|
||||
|
||||
CSConsole_Assembly_Blacklist = new("CSharp Console Assembly Blacklist",
|
||||
CSConsole_Assembly_Blacklist = new("CSharp Console Assembly Blacklist",
|
||||
"Use this to blacklist Assembly names from being referenced by the C# Console. Requires a Reset of the C# Console.\n" +
|
||||
"Separate each Assembly with a semicolon ';'." +
|
||||
"For example, to blacklist Assembly-CSharp, you would add 'Assembly-CSharp;'",
|
||||
@@ -177,7 +182,7 @@ namespace UnityExplorer.Config
|
||||
"Seperate signatures with a semicolon ';'.\r\n" +
|
||||
"For example, to blacklist Camera.main, you would add 'UnityEngine.Camera.main;'",
|
||||
"");
|
||||
|
||||
|
||||
Reflection_Hide_NativeInfoPtrs = new("Hide NativeMethodInfoPtr_s and NativeFieldInfoPtr_s",
|
||||
"Use this to blacklist NativeMethodPtr_s and NativeFieldInfoPtrs_s from the class inspector, mainly to reduce clutter.\r\n" +
|
||||
"For example, this will hide 'Class.NativeFieldInfoPtr_value' for the field 'Class.value'.",
|
||||
@@ -186,19 +191,23 @@ namespace UnityExplorer.Config
|
||||
Auto_Scale_UI = new("Make the mod UI automatically scale with resolution",
|
||||
"Especially useful when running games in high resolutions and you are having a hard time reading the mods menu (requires restart).",
|
||||
true);
|
||||
|
||||
|
||||
Reset_Camera_Transform = new("Reset Camera transform on freecam disable",
|
||||
"Reset the camera position and rotation between freecam sessions, so the freecam always starts from the gameplay position and rotation.",
|
||||
false);
|
||||
true);
|
||||
|
||||
Arrow_Size = new("Visualizers arrows size",
|
||||
"Cam Paths nodes and Lights Manager lights visualizers' arrow size (must be positive) (needs visualizer toggled to reflect changes).",
|
||||
1f);
|
||||
|
||||
Default_Freecam = new("Default Freecam mode",
|
||||
"Default type of freecam selected on startup.",
|
||||
FreeCamPanel.FreeCameraType.New);
|
||||
Freecam_Camera_Target_Selection = new("Freecam Camera Target Selection",
|
||||
"Enables certain advanced settings on the Freecam panel, in case the user can't get the freecam to work properly (requires game reset).",
|
||||
false);
|
||||
|
||||
Pause = new("Pause",
|
||||
"Toggle the pause of the game.",
|
||||
KeyCode.PageUp);
|
||||
|
||||
|
||||
Frameskip = new("Frameskip",
|
||||
"Skip a frame when the game is paused.",
|
||||
KeyCode.PageDown);
|
||||
@@ -252,7 +261,7 @@ namespace UnityExplorer.Config
|
||||
Left_1 = new("Left 1",
|
||||
"Move the freecam to the left.",
|
||||
KeyCode.A);
|
||||
|
||||
|
||||
Left_2 = new("Left 2",
|
||||
"Move the freecam to the left, alt key.",
|
||||
KeyCode.LeftArrow);
|
||||
@@ -300,6 +309,19 @@ namespace UnityExplorer.Config
|
||||
Toggle_Animations = new("Toggle NPC animations",
|
||||
"Toggle NPC animations as selected in the Animator panel.",
|
||||
KeyCode.Keypad0);
|
||||
|
||||
Default_Freecam = new("Default Freecam mode",
|
||||
"Default type of freecam selected on startup (gets automatically updated with the last type of camera used).",
|
||||
FreeCamPanel.FreeCameraType.ForcedMatrix);
|
||||
|
||||
Custom_Components_To_Disable = new("Custom components to disable",
|
||||
"List of custom components to disable when enabling the freecam (gets automatically updated when editing it from the freecam panel).",
|
||||
"");
|
||||
|
||||
Preferred_Target_Camera = new("Preferred Target Camera",
|
||||
"The camera that will be targeted by the freecam methods.\n" +
|
||||
"Only used when Freecam Camera Target Selection is enabled.",
|
||||
"\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace UnityExplorer
|
||||
|
||||
if (IInputManager.GetKeyDown(ConfigManager.Freecam_Toggle.Value))
|
||||
{
|
||||
FreeCamPanel.StartStopButton_OnClick();
|
||||
FreeCamPanel.ToggleFreecam();
|
||||
}
|
||||
|
||||
if (IInputManager.GetKeyDown(ConfigManager.Block_Freecam_Movement.Value))
|
||||
@@ -148,6 +148,30 @@ namespace UnityExplorer
|
||||
{
|
||||
FreeCamPanel.blockGamesInputOnFreecamToggle.isOn = !FreeCamPanel.blockGamesInputOnFreecamToggle.isOn;
|
||||
}
|
||||
|
||||
// Gamepad controls
|
||||
bool xButtonPressed = IGamepadInputInterceptor.IsButtonPressed("/gamepad/buttonwest");
|
||||
bool leftStickButton = IGamepadInputInterceptor.IsButtonPressed("/gamepad/leftstickpress");
|
||||
bool rightStickButton = IGamepadInputInterceptor.IsButtonPressed("/gamepad/rightstickpress");
|
||||
|
||||
// L3 + X: Toggle time scale override
|
||||
if (xButtonPressed && leftStickButton)
|
||||
{
|
||||
if (IGamepadInputInterceptor.WasButtonPressedThisFrame("/gamepad/buttonwest") ||
|
||||
IGamepadInputInterceptor.WasButtonPressedThisFrame("/gamepad/leftstickpress"))
|
||||
{
|
||||
UIManager.GetTimeScaleWidget().ToggleOverride();
|
||||
}
|
||||
}
|
||||
// R3 + X: Toggle freecam
|
||||
else if (xButtonPressed && rightStickButton)
|
||||
{
|
||||
if (IGamepadInputInterceptor.WasButtonPressedThisFrame("/gamepad/buttonwest") ||
|
||||
IGamepadInputInterceptor.WasButtonPressedThisFrame("/gamepad/rightstickpress"))
|
||||
{
|
||||
FreeCamPanel.ToggleFreecam();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void stopFrameSkip(){
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ namespace UnityExplorer
|
||||
public static class ExplorerCore
|
||||
{
|
||||
public const string NAME = "CinematicUnityExplorer";
|
||||
public const string VERSION = "1.3.0";
|
||||
public const string VERSION = "1.4.0";
|
||||
public const string AUTHOR = "originalnicodr, Sinai, yukieiji";
|
||||
public const string GUID = "com.originalnicodr.cinematicunityexplorer";
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ namespace UnityExplorer
|
||||
|
||||
public static void InspectWithFilters(object target, string filterInputField, UnityExplorer.Inspectors.MemberFilter memberFilterFlags = UnityExplorer.Inspectors.MemberFilter.All, bool staticReflection = false, CacheObjectBase parent = null)
|
||||
{
|
||||
if (TryFocusActiveInspector(target))
|
||||
return;
|
||||
|
||||
ReflectionInspector inspector = CreateInspector<ReflectionInspector>(target, staticReflection, parent);
|
||||
inspector.filterInputField.Text = filterInputField;
|
||||
//TODO: Update the member flags visually on the inspector.
|
||||
|
||||
@@ -46,9 +46,10 @@ namespace UnityExplorer.Inspectors
|
||||
public override bool CanDragAndResize => false;
|
||||
private Action<GameObject> inspectorAction = null;
|
||||
|
||||
internal Text objNameLabel;
|
||||
internal Text objPathLabel;
|
||||
internal Text mousePosLabel;
|
||||
private Text inspectorLabelTitle;
|
||||
private Text objNameLabel;
|
||||
private Text objPathLabel;
|
||||
private Text mousePosLabel;
|
||||
|
||||
public MouseInspector(UIBase owner) : base(owner)
|
||||
{
|
||||
@@ -125,6 +126,43 @@ namespace UnityExplorer.Inspectors
|
||||
return Inspecting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the title text in the inspector UI, if the inspector title label is assigned.
|
||||
/// </summary>
|
||||
/// <param name="title">The new title text to display in the inspector.</param>
|
||||
internal void UpdateInspectorTitle(string title)
|
||||
{
|
||||
// Unity null check - if inspectorLabelTitle is assigned, update its text.
|
||||
if (inspectorLabelTitle)
|
||||
{
|
||||
inspectorLabelTitle.text = title;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the object name label in the inspector UI, if the label is assigned.
|
||||
/// </summary>
|
||||
/// <param name="name">The new object name to display.</param>
|
||||
internal void UpdateObjectNameLabel(string name)
|
||||
{
|
||||
// Unity null check - if objNameLabel is assigned, update its text.
|
||||
if (objNameLabel)
|
||||
{
|
||||
objNameLabel.text = name;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates the object path label in the inspector UI, if the label is assigned.
|
||||
/// </summary>
|
||||
/// <param name="path">The new object path to display.</param>
|
||||
internal void UpdateObjectPathLabel(string path)
|
||||
{
|
||||
// Unity null check - if objPathLabel is assigned, update its text.
|
||||
if (objPathLabel)
|
||||
{
|
||||
objPathLabel.text = path;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateInspect()
|
||||
{
|
||||
if (IInputManager.GetKeyDown(KeyCode.Escape))
|
||||
@@ -156,7 +194,7 @@ namespace UnityExplorer.Inspectors
|
||||
lastMousePos = mousePos;
|
||||
|
||||
// use the raw mouse pos for the label
|
||||
mousePosLabel.text = $"<color=grey>Mouse Position:</color> {mousePos.ToString()}";
|
||||
mousePosLabel.text = $"<color=grey>Mouse Position:</color> {mousePos.x}, {mousePos.y}";
|
||||
|
||||
// constrain the mouse pos we use within certain bounds
|
||||
if (mousePos.x < 350)
|
||||
@@ -196,11 +234,11 @@ namespace UnityExplorer.Inspectors
|
||||
|
||||
// Title text
|
||||
|
||||
Text title = UIFactory.CreateLabel(inspectContent,
|
||||
inspectorLabelTitle = UIFactory.CreateLabel(inspectContent,
|
||||
"InspectLabel",
|
||||
"<b>Mouse Inspector</b> (press <b>ESC</b> to cancel)",
|
||||
"",
|
||||
TextAnchor.MiddleCenter);
|
||||
UIFactory.SetLayoutElement(title.gameObject, flexibleWidth: 9999);
|
||||
UIFactory.SetLayoutElement(inspectorLabelTitle.gameObject, flexibleWidth: 9999);
|
||||
|
||||
mousePosLabel = UIFactory.CreateLabel(inspectContent, "MousePosLabel", "Mouse Position:", TextAnchor.MiddleCenter);
|
||||
|
||||
|
||||
@@ -17,10 +17,13 @@ namespace UnityExplorer.Inspectors.MouseInspectors
|
||||
private static readonly List<CanvasGroup> wasDisabledCanvasGroups = new();
|
||||
private static readonly List<GameObject> objectsAddedCastersTo = new();
|
||||
|
||||
private const string DEFAULT_INSPECTOR_TITLE = "<b>UI Inspector</b> (press <b>ESC</b> to cancel)";
|
||||
|
||||
public override void OnBeginMouseInspect()
|
||||
{
|
||||
SetupUIRaycast();
|
||||
MouseInspector.Instance.objPathLabel.text = "";
|
||||
MouseInspector.Instance.UpdateInspectorTitle(DEFAULT_INSPECTOR_TITLE);
|
||||
MouseInspector.Instance.UpdateObjectPathLabel("");
|
||||
}
|
||||
|
||||
public override void ClearHitData()
|
||||
@@ -71,9 +74,9 @@ namespace UnityExplorer.Inspectors.MouseInspectors
|
||||
}
|
||||
|
||||
if (currentHitObjects.Any())
|
||||
MouseInspector.Instance.objNameLabel.text = $"Click to view UI Objects under mouse: {currentHitObjects.Count}";
|
||||
MouseInspector.Instance.UpdateObjectNameLabel($"Click to view UI Objects under mouse: {currentHitObjects.Count}");
|
||||
else
|
||||
MouseInspector.Instance.objNameLabel.text = $"No UI objects under mouse.";
|
||||
MouseInspector.Instance.UpdateObjectNameLabel( $"No UI objects under mouse.");
|
||||
}
|
||||
|
||||
private static void SetupUIRaycast()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace UnityExplorer.Inspectors.MouseInspectors
|
||||
using UnityExplorer.UI.Panels;
|
||||
|
||||
namespace UnityExplorer.Inspectors.MouseInspectors
|
||||
{
|
||||
public class WorldInspector : MouseInspectorBase
|
||||
{
|
||||
@@ -7,15 +9,26 @@
|
||||
|
||||
public override void OnBeginMouseInspect()
|
||||
{
|
||||
MainCamera = Camera.main;
|
||||
|
||||
if (!MainCamera)
|
||||
if (!EnsureMainCamera())
|
||||
{
|
||||
ExplorerCore.LogWarning("No MainCamera found! Cannot inspect world!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns it as the MainCamera and updates the inspector title.
|
||||
/// </summary>
|
||||
/// <param name="cam">The camera to assign.</param>
|
||||
private static void AssignCamAndUpdateTitle(Camera cam)
|
||||
{
|
||||
MainCamera = cam;
|
||||
MouseInspector.Instance.UpdateInspectorTitle(
|
||||
$"<b>World Inspector ({MainCamera.name})</b> (press <b>ESC</b> to cancel)"
|
||||
);
|
||||
}
|
||||
|
||||
public override void ClearHitData()
|
||||
{
|
||||
lastHitObject = null;
|
||||
@@ -26,11 +39,52 @@
|
||||
inspectorAction(lastHitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to ensure that MainCamera is assigned. If not then attempts to find it.
|
||||
/// If no cameras are available, logs a warning and returns null.
|
||||
/// </summary>
|
||||
private static Camera EnsureMainCamera()
|
||||
{
|
||||
if (MainCamera){
|
||||
// We still call this in case the last title was from the UIInspector
|
||||
AssignCamAndUpdateTitle(MainCamera);
|
||||
return MainCamera;
|
||||
}
|
||||
|
||||
if (Camera.main){
|
||||
AssignCamAndUpdateTitle(Camera.main);
|
||||
return MainCamera;
|
||||
}
|
||||
|
||||
ExplorerCore.LogWarning("No Camera.main found, trying to find a camera named 'Main Camera' or 'MainCamera'...");
|
||||
Camera namedCam = Camera.allCameras.FirstOrDefault(c => c.name is "Main Camera" or "MainCamera");
|
||||
if (namedCam) {
|
||||
AssignCamAndUpdateTitle(namedCam);
|
||||
return MainCamera;
|
||||
}
|
||||
|
||||
if (FreeCamPanel.inFreeCamMode && FreeCamPanel.GetFreecam()){
|
||||
AssignCamAndUpdateTitle(FreeCamPanel.GetFreecam());
|
||||
return MainCamera;
|
||||
}
|
||||
|
||||
ExplorerCore.LogWarning("No camera named found, using the first camera created...");
|
||||
var fallbackCam = Camera.allCameras.FirstOrDefault();
|
||||
if (fallbackCam) {
|
||||
AssignCamAndUpdateTitle(fallbackCam);
|
||||
return MainCamera;
|
||||
}
|
||||
|
||||
// If we reach here, no cameras were found at all.
|
||||
ExplorerCore.LogWarning("No valid cameras found!");
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void UpdateMouseInspect(Vector2 mousePos)
|
||||
{
|
||||
if (!MainCamera)
|
||||
MainCamera = Camera.main;
|
||||
if (!MainCamera)
|
||||
// Attempt to ensure camera each time UpdateMouseInspect is called
|
||||
// in case something changed or wasn't set initially.
|
||||
if (!EnsureMainCamera())
|
||||
{
|
||||
ExplorerCore.LogWarning("No Main Camera was found, unable to inspect world!");
|
||||
MouseInspector.Instance.StopInspect();
|
||||
@@ -51,8 +105,8 @@
|
||||
if (obj != lastHitObject)
|
||||
{
|
||||
lastHitObject = obj;
|
||||
MouseInspector.Instance.objNameLabel.text = $"<b>Click to Inspect:</b> <color=cyan>{obj.name}</color>";
|
||||
MouseInspector.Instance.objPathLabel.text = $"Path: {obj.transform.GetTransformPath(true)}";
|
||||
MouseInspector.Instance.UpdateObjectNameLabel($"<b>Click to Inspect:</b> <color=cyan>{obj.name}</color>");
|
||||
MouseInspector.Instance.UpdateObjectPathLabel($"Path: {obj.transform.GetTransformPath(true)}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ namespace UnityExplorer.Loader.Standalone
|
||||
public bool Auto_Scale_UI;
|
||||
public bool Reset_Camera_Transform;
|
||||
public FreeCamPanel.FreeCameraType Default_Freecam;
|
||||
public string Custom_Components_To_Disable;
|
||||
public string Preferred_Target_Camera;
|
||||
public float Arrow_Size = 1f;
|
||||
public bool Freecam_Camera_Target_Selection;
|
||||
|
||||
public KeyCode Pause;
|
||||
public KeyCode Frameskip;
|
||||
@@ -87,6 +91,11 @@ namespace UnityExplorer.Loader.Standalone
|
||||
ConfigManager.Auto_Scale_UI.Value = this.Auto_Scale_UI;
|
||||
ConfigManager.Reset_Camera_Transform.Value = this.Reset_Camera_Transform;
|
||||
ConfigManager.Default_Freecam.Value = this.Default_Freecam;
|
||||
ConfigManager.Custom_Components_To_Disable.Value = this.Custom_Components_To_Disable;
|
||||
ConfigManager.Preferred_Target_Camera.Value = this.Preferred_Target_Camera;
|
||||
|
||||
ConfigManager.Arrow_Size.Value = this.Arrow_Size;
|
||||
ConfigManager.Freecam_Camera_Target_Selection.Value = this.Freecam_Camera_Target_Selection;
|
||||
|
||||
ConfigManager.Pause.Value = this.Pause;
|
||||
ConfigManager.Frameskip.Value = this.Frameskip;
|
||||
|
||||
+77
-13
@@ -43,6 +43,7 @@ namespace UnityExplorer.UI.Panels
|
||||
public List<CatmullRom.CatmullRomPoint> controlPoints = new List<CatmullRom.CatmullRomPoint>();
|
||||
bool closedLoop;
|
||||
float time = 10;
|
||||
public int currentSelectedNodeIndex = -1;
|
||||
|
||||
public GameObject followObject;
|
||||
|
||||
@@ -99,6 +100,8 @@ namespace UnityExplorer.UI.Panels
|
||||
cell.point = point;
|
||||
cell.index = index;
|
||||
cell.indexLabel.text = $"{index}";
|
||||
|
||||
cell.indexLabel.color = (index == currentSelectedNodeIndex) ? Color.green : Color.white;
|
||||
}
|
||||
|
||||
// ~~~~~~~~ UI construction / callbacks ~~~~~~~~
|
||||
@@ -122,10 +125,10 @@ namespace UnityExplorer.UI.Panels
|
||||
stopButton.ButtonText.fontSize = 20;
|
||||
stopButton.OnClick += Stop_OnClick;
|
||||
|
||||
ButtonRef AddNode = UIFactory.CreateButton(horiGroup, "AddCamNode", "+");
|
||||
UIFactory.SetLayoutElement(AddNode.GameObject, minWidth: 50, minHeight: 25);
|
||||
AddNode.ButtonText.fontSize = 20;
|
||||
AddNode.OnClick += AddNode_OnClick;
|
||||
ButtonRef AddNodeButton = UIFactory.CreateButton(horiGroup, "AddCamNode", "+");
|
||||
UIFactory.SetLayoutElement(AddNodeButton.GameObject, minWidth: 50, minHeight: 25);
|
||||
AddNodeButton.ButtonText.fontSize = 20;
|
||||
AddNodeButton.OnClick += AddNode;
|
||||
|
||||
ButtonRef DeletePath = UIFactory.CreateButton(horiGroup, "DeletePath", "Clear");
|
||||
UIFactory.SetLayoutElement(DeletePath.GameObject, minWidth: 70, minHeight: 25);
|
||||
@@ -337,20 +340,22 @@ namespace UnityExplorer.UI.Panels
|
||||
|
||||
void StartButton_OnClick()
|
||||
{
|
||||
StartPath();
|
||||
}
|
||||
|
||||
public void StartPath(){
|
||||
if (waitBeforePlay) {
|
||||
if (startTimer.Enabled){
|
||||
startTimer.Stop();
|
||||
}
|
||||
|
||||
startTimer.Enabled = true;
|
||||
} else {
|
||||
StartPath();
|
||||
return;
|
||||
}
|
||||
|
||||
EventSystemHelper.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
void StartPath(){
|
||||
|
||||
if (GetCameraPathsManager()){
|
||||
UpdateCatmullRomMoverData();
|
||||
GetCameraPathsManager().StartPath();
|
||||
@@ -363,7 +368,7 @@ namespace UnityExplorer.UI.Panels
|
||||
}
|
||||
}
|
||||
|
||||
void TogglePause_OnClick(){
|
||||
public void TogglePause(){
|
||||
if(GetCameraPathsManager()){
|
||||
GetCameraPathsManager().TogglePause();
|
||||
if (visualizePathToggle.isOn && GetCameraPathsManager().IsPaused()) visualizePathToggle.isOn = false;
|
||||
@@ -372,7 +377,11 @@ namespace UnityExplorer.UI.Panels
|
||||
EventSystemHelper.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
void Stop_OnClick(){
|
||||
void TogglePause_OnClick(){
|
||||
TogglePause();
|
||||
}
|
||||
|
||||
public void StopPath(){
|
||||
if (GetCameraPathsManager()){
|
||||
GetCameraPathsManager().Stop();
|
||||
}
|
||||
@@ -380,7 +389,11 @@ namespace UnityExplorer.UI.Panels
|
||||
EventSystemHelper.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
void AddNode_OnClick(){
|
||||
void Stop_OnClick(){
|
||||
StopPath();
|
||||
}
|
||||
|
||||
public void AddNode(){
|
||||
EventSystemHelper.SetSelectedGameObject(null);
|
||||
|
||||
CatmullRom.CatmullRomPoint point = new CatmullRom.CatmullRomPoint(
|
||||
@@ -390,6 +403,7 @@ namespace UnityExplorer.UI.Panels
|
||||
);
|
||||
|
||||
controlPoints.Add(point);
|
||||
currentSelectedNodeIndex = controlPoints.Count - 1;
|
||||
nodesScrollPool.Refresh(true, false);
|
||||
MaybeRedrawPath();
|
||||
}
|
||||
@@ -528,6 +542,56 @@ namespace UnityExplorer.UI.Panels
|
||||
File.WriteAllText($"{camPathsFolderPath}\\{filename}.cuepath", serializedData);
|
||||
}
|
||||
|
||||
void NavigateToNode(int nodeIndex)
|
||||
{
|
||||
if (nodeIndex < 0 || nodeIndex >= controlPoints.Count)
|
||||
return;
|
||||
|
||||
currentSelectedNodeIndex = nodeIndex;
|
||||
CatmullRom.CatmullRomPoint point = controlPoints[nodeIndex];
|
||||
FreeCamPanel.SetCameraPosition(point.position);
|
||||
FreeCamPanel.SetCameraRotation(point.rotation);
|
||||
FreeCamPanel.SetFOV(point.fov);
|
||||
nodesScrollPool.Refresh(true, false);
|
||||
}
|
||||
|
||||
public void NavigateToNextNode()
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return;
|
||||
|
||||
int nextIndex = (currentSelectedNodeIndex + 1) % controlPoints.Count;
|
||||
NavigateToNode(nextIndex);
|
||||
}
|
||||
|
||||
public void NavigateToPreviousNode()
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return;
|
||||
|
||||
int prevIndex = currentSelectedNodeIndex - 1;
|
||||
if (prevIndex < 0)
|
||||
prevIndex = controlPoints.Count - 1;
|
||||
NavigateToNode(prevIndex);
|
||||
}
|
||||
|
||||
public void OnNodeDeleted(int deletedIndex)
|
||||
{
|
||||
// If the current selected index is out of bounds, adjust it
|
||||
if (currentSelectedNodeIndex >= controlPoints.Count)
|
||||
{
|
||||
currentSelectedNodeIndex = controlPoints.Count - 1;
|
||||
}
|
||||
// If we deleted the selected node or a node before it, adjust the index
|
||||
else if (deletedIndex <= currentSelectedNodeIndex && currentSelectedNodeIndex > 0)
|
||||
{
|
||||
currentSelectedNodeIndex--;
|
||||
}
|
||||
|
||||
if (controlPoints.Count == 0)
|
||||
currentSelectedNodeIndex = -1;
|
||||
}
|
||||
|
||||
private void LoadPath(){
|
||||
string filename = saveLoadInputField.Component.text;
|
||||
if (filename.EndsWith(".cuepath") || filename.EndsWith(".CUEPATH")) filename = filename.Substring(filename.Length-7);
|
||||
@@ -563,12 +627,12 @@ namespace UnityExplorer.UI.Panels
|
||||
|
||||
// We enable the freecam so we can use it to spawn the camera path relative to it
|
||||
if (!FreeCamPanel.inFreeCamMode) {
|
||||
FreeCamPanel.StartStopButton_OnClick();
|
||||
FreeCamPanel.ToggleFreecam();
|
||||
}
|
||||
|
||||
controlPoints.Clear();
|
||||
// The first point will be the camera position, and we adapt the following points from there
|
||||
AddNode_OnClick();
|
||||
AddNode();
|
||||
|
||||
CatmullRom.CatmullRomPoint startingPoint = controlPoints.ElementAt(0);
|
||||
CatmullRom.CatmullRomPoint originalStartingPoint = deserializedObj.points.ElementAt(0);
|
||||
|
||||
+778
-124
File diff suppressed because it is too large
Load Diff
@@ -122,7 +122,8 @@ namespace UnityExplorer.CatmullRom
|
||||
public void StartPath(){
|
||||
if(splinePoints == null || splinePoints.Length <= 2)
|
||||
{
|
||||
ExplorerCore.Log(splinePoints);
|
||||
int nodeCount = splinePoints?.Length ?? 0;
|
||||
ExplorerCore.LogWarning($"Cannot start camera path: Need at least 3 nodes, but only {nodeCount} node(s) exist. Please add more nodes to create a path.");
|
||||
throw new ArgumentException("Catmull Rom Error: Too few spline points!");
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -68,6 +68,27 @@ namespace UnityExplorer.UI.Widgets
|
||||
settingTimeScale = false;
|
||||
}
|
||||
|
||||
public void IncreaseTimeScale()
|
||||
{
|
||||
float newValue = Mathf.Min(desiredTime + 0.1f, slider.maxValue);
|
||||
slider.value = newValue;
|
||||
if (overrideTimeScaleToggle.isOn)
|
||||
SetTimeScale(newValue);
|
||||
}
|
||||
|
||||
public void DecreaseTimeScale()
|
||||
{
|
||||
float newValue = Mathf.Max(desiredTime - 0.1f, slider.minValue);
|
||||
slider.value = newValue;
|
||||
if (overrideTimeScaleToggle.isOn)
|
||||
SetTimeScale(newValue);
|
||||
}
|
||||
|
||||
public void ToggleOverride()
|
||||
{
|
||||
overrideTimeScaleToggle.isOn = !overrideTimeScaleToggle.isOn;
|
||||
}
|
||||
|
||||
// UI event listeners
|
||||
|
||||
void OnTimeInputEndEdit(string val)
|
||||
|
||||
Reference in New Issue
Block a user