WIP 1.2.0 - untested

This commit is contained in:
Terkoiz 2022-06-03 14:28:35 +03:00
parent d28425626d
commit b6092fee94
5 changed files with 281 additions and 62 deletions

View File

@ -1,4 +1,5 @@
using UnityEngine; using JetBrains.Annotations;
using UnityEngine;
namespace Terkoiz.Freecam namespace Terkoiz.Freecam
{ {
@ -13,7 +14,8 @@ namespace Terkoiz.Freecam
{ {
public bool IsActive = false; public bool IsActive = false;
void Update() [UsedImplicitly]
public void Update()
{ {
if (!IsActive) if (!IsActive)
{ {
@ -43,6 +45,8 @@ namespace Terkoiz.Freecam
transform.position += (-transform.forward * movementSpeed * Time.deltaTime); transform.position += (-transform.forward * movementSpeed * Time.deltaTime);
} }
if (FreecamPlugin.CameraHeightMovement.Value)
{
if (Input.GetKey(KeyCode.Q)) if (Input.GetKey(KeyCode.Q))
{ {
transform.position += (transform.up * movementSpeed * Time.deltaTime); transform.position += (transform.up * movementSpeed * Time.deltaTime);
@ -62,11 +66,14 @@ namespace Terkoiz.Freecam
{ {
transform.position += (-Vector3.up * movementSpeed * Time.deltaTime); transform.position += (-Vector3.up * movementSpeed * Time.deltaTime);
} }
}
float newRotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * FreecamPlugin.CameraLookSensitivity.Value; float newRotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * FreecamPlugin.CameraLookSensitivity.Value;
float newRotationY = transform.localEulerAngles.x - Input.GetAxis("Mouse Y") * FreecamPlugin.CameraLookSensitivity.Value; float newRotationY = transform.localEulerAngles.x - Input.GetAxis("Mouse Y") * FreecamPlugin.CameraLookSensitivity.Value;
transform.localEulerAngles = new Vector3(newRotationY, newRotationX, 0f); transform.localEulerAngles = new Vector3(newRotationY, newRotationX, 0f);
if (FreecamPlugin.CameraMousewheelZoom.Value)
{
float axis = Input.GetAxis("Mouse ScrollWheel"); float axis = Input.GetAxis("Mouse ScrollWheel");
if (axis != 0) if (axis != 0)
{ {
@ -76,3 +83,4 @@ namespace Terkoiz.Freecam
} }
} }
} }
}

View File

@ -1,23 +1,55 @@
using Comfort.Common; using System.Reflection;
using Comfort.Common;
using EFT; using EFT;
using EFT.CameraControl;
using EFT.UI; using EFT.UI;
using HarmonyLib;
using JetBrains.Annotations;
using MonoMod.RuntimeDetour;
using UnityEngine; using UnityEngine;
namespace Terkoiz.Freecam namespace Terkoiz.Freecam
{ {
public class FreecamController : MonoBehaviour public class FreecamController : MonoBehaviour
{ {
private GameObject mainCamera; private GameObject _mainCamera;
private Freecam freeCamScript; private Freecam _freeCamScript;
private BattleUIScreen playerUi; private BattleUIScreen _playerUi;
private bool uiHidden; private bool _uiHidden;
// TODO MAYBE: private MethodInfo _playerMoveMethod;
// Hide player weapon private MethodInfo _playerRotateMethod;
private Detour _moveDetour;
private Detour _rotateDetour;
private Vector3? _lastPosition;
private Quaternion? _lastRotation;
// TODO:
// Hide version number UI element // Hide version number UI element
// FreeCam controller support (camera could be smoother with an analog stick, apparently) // Button to toggle between camera and player movement
// Independent FoV setting for Freecam mode (_mainCamera.GetComponent<Camera>().fieldOfView = ...)
[UsedImplicitly]
public void Start()
{
// We locate and get the MethodInfo for the Move method that we will later detour to prevent the player from moving during Freecam
_playerMoveMethod = typeof(Player).GetMethod("Move");
if (_playerMoveMethod == null)
{
FreecamPlugin.Logger.LogError("Failed to locate the Player.Move method!");
}
// Same deal here, but for player rotation
_playerRotateMethod = typeof(Player).GetMethod("Rotate");
if (_playerRotateMethod == null)
{
FreecamPlugin.Logger.LogError("Failed to locate the Player.Rotate method!");
}
}
[UsedImplicitly]
public void Update() public void Update()
{ {
if (FreecamPlugin.ToggleUi.Value.IsDown()) if (FreecamPlugin.ToggleUi.Value.IsDown())
@ -49,36 +81,33 @@ namespace Terkoiz.Freecam
} }
// If we don't have the main camera object cached, go look for it in the scene // If we don't have the main camera object cached, go look for it in the scene
if (mainCamera == null) if (_mainCamera == null)
{ {
// Finding a GameObject by name directly is apparantly better than searching for objects of a type. // Finding a GameObject by name directly is apparently better than searching for objects of a type.
// 'FPS Camera' is our main camera instance - so we just grab that // 'FPS Camera' is our main camera instance - so we just grab that
mainCamera = GameObject.Find("FPS Camera"); _mainCamera = GameObject.Find("FPS Camera");
if (mainCamera == null) if (_mainCamera == null)
{ {
Debug.LogError("Terkoiz.Freecam: ERROR: Failed to locate main camera"); FreecamPlugin.Logger.LogError("Failed to locate main camera");
PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate main camera", "FreeCam"); PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate main camera", "FreeCam");
return; return;
} }
} }
// Create the Freecam script and attach it to the main camera // Create the Freecam script and attach it to the main camera
if (freeCamScript == null) if (_freeCamScript == null)
{ {
freeCamScript = mainCamera.AddComponent<Freecam>(); ClearDetours();
_freeCamScript = _mainCamera.AddComponent<Freecam>();
} }
// We disable the player's GameObject, which prevents the player from moving and interacting with the world while we're in Freecam mode if (!_freeCamScript.IsActive)
// Also, since the camera position keeps getting updated to where the player is, disabling the player's GameObject also stops this behaviour
if (!freeCamScript.IsActive)
{ {
localPlayer.gameObject.SetActive(false); SetPlayerToFreecamMode(localPlayer);
freeCamScript.IsActive = true;
} }
else else
{ {
freeCamScript.IsActive = false; SetPlayerToFirstPersonMode(localPlayer);
localPlayer.gameObject.SetActive(true);
} }
} }
@ -94,19 +123,19 @@ namespace Terkoiz.Freecam
} }
// If we don't have the main camera cached, it means we have yet to enter Freecam mode and there is nowhere to teleport to // If we don't have the main camera cached, it means we have yet to enter Freecam mode and there is nowhere to teleport to
if (mainCamera == null) if (_mainCamera == null)
{ {
return; return;
} }
// We basically do what ToggleCamera() does, but with extra code inbetween // Move the player to the camera's current position and switch to First Person mode
if (freeCamScript.IsActive) if (_freeCamScript.IsActive)
{ {
freeCamScript.IsActive = false;
// We grab the camera's position, but we subtract a bit off the Y axis, because the players coordinate origin is at the feet // We grab the camera's position, but we subtract a bit off the Y axis, because the players coordinate origin is at the feet
var position = new Vector3(mainCamera.transform.position.x, mainCamera.transform.position.y - 1.8f, mainCamera.transform.position.z); var position = new Vector3(_mainCamera.transform.position.x, _mainCamera.transform.position.y - 1.8f, _mainCamera.transform.position.z);
localPlayer.gameObject.transform.SetPositionAndRotation(position, mainCamera.transform.rotation); localPlayer.gameObject.transform.SetPositionAndRotation(position, Quaternion.Euler(0, _mainCamera.transform.rotation.y, 0));
localPlayer.gameObject.SetActive(true); // localPlayer.gameObject.transform.SetPositionAndRotation(position, _mainCamera.transform.rotation);
SetPlayerToFirstPersonMode(localPlayer);
} }
} }
@ -122,26 +151,97 @@ namespace Terkoiz.Freecam
} }
// If we don't have the UI Component cached, go look for it in the scene // If we don't have the UI Component cached, go look for it in the scene
if (playerUi == null) if (_playerUi == null)
{ {
playerUi = GameObject.Find("BattleUIScreen").GetComponent<BattleUIScreen>(); _playerUi = GameObject.Find("BattleUIScreen").GetComponent<BattleUIScreen>();
if (playerUi == null) if (_playerUi == null)
{ {
FreecamPlugin.Logger.LogError("Failed to locate player UI");
PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate player UI", "FreeCam"); PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate player UI", "FreeCam");
return; return;
} }
} }
playerUi.gameObject.SetActive(uiHidden); _playerUi.gameObject.SetActive(_uiHidden);
uiHidden = !uiHidden; _uiHidden = !_uiHidden;
}
/// <summary>
/// A helper method to set the Player into Freecam mode
/// </summary>
/// <param name="localPlayer"></param>
private void SetPlayerToFreecamMode(Player localPlayer)
{
// We set the player to third person mode, but then we want set the camera to freecam mode
// This means our character will be fully visible, while letting the camera move freely
// Setting the player point of view directly to Freecam seems to hide the head and arms of the character, which is not desirable
localPlayer.PointOfView = EPointOfView.ThirdPerson;
// Get the PlayerBody reference. It's a protected field, so we have to use traverse to fetch it
var playerBody = Traverse.Create(localPlayer).Field<PlayerBody>("_playerBody").Value;
if (playerBody != null)
{
// We tell the PlayerBody class that it's in FreeCamera mode, and force an update of the Camera Controller view mode
// Setting the PointOfView.Value directly skips all the code that would usually change how the player body is rendered
playerBody.PointOfView.Value = EPointOfView.FreeCamera;
localPlayer.GetComponent<PlayerCameraController>().UpdatePointOfView();
// All we really needed to do, was trigger the UpdatePointOfView method and have it update to the FreeCam state
// There's no easy way of doing this without patching the method, and even then it might be a bloated solution
}
else
{
FreecamPlugin.Logger.LogError("Failed to get the PlayerBody field");
PreloaderUI.Instance.Console.AddLog("ERROR: Failed to get the PlayerBody field", "FreeCam");
}
// Detour the Player.Move method into an empty one, preventing the character from moving during Freecam mode
if (_playerMoveMethod != null)
{
_moveDetour = new Detour(_playerMoveMethod, typeof(FreecamController).GetMethod(nameof(DisabledMove)));
}
// Same deal here, but for player rotation
if (_playerRotateMethod != null)
{
_rotateDetour = new Detour(_playerRotateMethod, typeof(FreecamController).GetMethod(nameof(DisabledRotate)));
}
if (FreecamPlugin.CameraRememberLastPosition.Value && _lastPosition != null && _lastRotation != null)
{
_mainCamera.transform.position = _lastPosition.Value;
_mainCamera.transform.rotation = _lastRotation.Value;
}
_freeCamScript.IsActive = true;
}
/// <summary>
/// A helper method to reset the player view back to First Person
/// </summary>
/// <param name="localPlayer"></param>
private void SetPlayerToFirstPersonMode(Player localPlayer)
{
_freeCamScript.IsActive = false;
if (FreecamPlugin.CameraRememberLastPosition.Value)
{
_lastPosition = _mainCamera.transform.position;
_lastRotation = _mainCamera.transform.rotation;
}
ClearDetours();
localPlayer.PointOfView = EPointOfView.FirstPerson;
} }
/// <summary> /// <summary>
/// Gets the current <see cref="Player"/> instance if it's available /// Gets the current <see cref="Player"/> instance if it's available
/// </summary> /// </summary>
/// <returns>Local <see cref="Player"/> instance; returns null if the game is not in raid</returns> /// <returns>Local <see cref="Player"/> instance; returns null if the game is not in raid</returns>
private static Player GetLocalPlayerFromWorld() private Player GetLocalPlayerFromWorld()
{ {
// If the GameWorld instance is null or has no RegisteredPlayers, it most likely means we're not in a raid // If the GameWorld instance is null or has no RegisteredPlayers, it most likely means we're not in a raid
var gameWorld = Singleton<GameWorld>.Instance; var gameWorld = Singleton<GameWorld>.Instance;
@ -150,8 +250,51 @@ namespace Terkoiz.Freecam
return null; return null;
} }
// One of the RegisterdPlayers will have the IsYourPlayer flag set, which will be our own Player instance // One of the RegisteredPlayers will have the IsYourPlayer flag set, which will be our own Player instance
return gameWorld.RegisteredPlayers.Find(p => p.IsYourPlayer); return gameWorld.RegisteredPlayers.Find(p => p.IsYourPlayer);
} }
[UsedImplicitly]
public void OnDestroy()
{
_freeCamScript.IsActive = false;
// Making sure no stray Detours are left
ClearDetours();
_lastPosition = null;
_lastRotation = null;
}
public void ClearDetours()
{
if (_moveDetour != null)
{
_moveDetour.Dispose();
_moveDetour = null;
}
if (_rotateDetour != null)
{
_rotateDetour.Dispose();
_rotateDetour = null;
}
}
/// <summary>
/// An empty method that's used to detour the player Move method, preventing movement during Freecam mode
/// </summary>
[UsedImplicitly]
public static void DisabledMove(Player self, Vector2 direction)
{
}
/// <summary>
/// An empty method that's used to detour the player Rotate method, preventing rotation during Freecam mode
/// </summary>
[UsedImplicitly]
public static void DisabledRotate(Player self, Vector2 deltaRotation, bool ignoreClamp = false)
{
}
} }
} }

View File

@ -1,12 +1,16 @@
using BepInEx; using BepInEx;
using BepInEx.Configuration; using BepInEx.Configuration;
using BepInEx.Logging;
using JetBrains.Annotations;
using UnityEngine; using UnityEngine;
namespace Terkoiz.Freecam namespace Terkoiz.Freecam
{ {
[BepInPlugin("com.terkoiz.freecam", "Terkoiz.Freecam", "1.1.1")] [BepInPlugin("com.terkoiz.freecam", "Terkoiz.Freecam", "1.2.0")]
public class FreecamPlugin : BaseUnityPlugin public class FreecamPlugin : BaseUnityPlugin
{ {
internal new static ManualLogSource Logger { get; private set; }
private static GameObject HookObject; private static GameObject HookObject;
// Keyboard shortcut config entries // Keyboard shortcut config entries
@ -23,13 +27,23 @@ namespace Terkoiz.Freecam
internal static ConfigEntry<float> CameraZoomSpeed; internal static ConfigEntry<float> CameraZoomSpeed;
internal static ConfigEntry<float> CameraFastZoomSpeed; internal static ConfigEntry<float> CameraFastZoomSpeed;
private void Awake() private const string TogglesSectionName = "Toggles";
internal static ConfigEntry<bool> CameraHeightMovement;
internal static ConfigEntry<bool> CameraMousewheelZoom;
internal static ConfigEntry<bool> CameraRememberLastPosition;
// TODO: Hook into camera OnDestroy to run the OnDestroy from FreecamController
[UsedImplicitly]
internal void Start()
{ {
Logger = base.Logger;
InitConfiguration(); InitConfiguration();
HookObject = new GameObject(); HookObject = new GameObject();
HookObject.AddComponent<FreecamController>(); HookObject.AddComponent<FreecamController>();
Object.DontDestroyOnLoad(HookObject); DontDestroyOnLoad(HookObject);
} }
private void InitConfiguration() private void InitConfiguration()
@ -91,6 +105,25 @@ namespace Terkoiz.Freecam
new ConfigDescription( new ConfigDescription(
"Amount to zoom the camera when using the mouse wheel while holding Shift", "Amount to zoom the camera when using the mouse wheel while holding Shift",
new AcceptableValueRange<float>(0.01f, 1000f))); new AcceptableValueRange<float>(0.01f, 1000f)));
CameraHeightMovement = Config.Bind(
TogglesSectionName,
"CameraHeightMovementKeys",
true,
"Enables or disables the camera height movement keys, which default to Q, E, R, F." +
" \nUseful to disable if you want to let your character lean in Freecam mode");
CameraMousewheelZoom = Config.Bind(
TogglesSectionName,
"CameraMousewheelZoom",
true,
"Enables or disables camera movement on mousewheel scroll. Just in case you find it annoying.");
CameraRememberLastPosition = Config.Bind(
TogglesSectionName,
"CameraRememberLastPosition",
false,
"If enabled, returning to Freecam mode will put the camera to it's last position which was saved when exiting Freecam mode.");
} }
} }
} }

View File

@ -13,6 +13,8 @@
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<TargetFrameworkProfile /> <TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -39,11 +41,14 @@
</AssemblyOriginatorKeyFile> </AssemblyOriginatorKeyFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="0Harmony, Version=2.7.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\HarmonyX.2.7.0\lib\net45\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp"> <Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Hollowed\Assembly-CSharp.dll</HintPath> <HintPath>..\Shared\Hollowed\Assembly-CSharp.dll</HintPath>
</Reference> </Reference>
<Reference Include="BepInEx, Version=5.4.17.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="BepInEx, Version=5.4.19.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\BepInEx.BaseLib.5.4.17\lib\net35\BepInEx.dll</HintPath> <HintPath>..\packages\BepInEx.BaseLib.5.4.19\lib\net35\BepInEx.dll</HintPath>
</Reference> </Reference>
<Reference Include="Comfort"> <Reference Include="Comfort">
<HintPath>..\Shared\Managed\Comfort.dll</HintPath> <HintPath>..\Shared\Managed\Comfort.dll</HintPath>
@ -51,6 +56,24 @@
<Reference Include="ItemComponent.Types"> <Reference Include="ItemComponent.Types">
<HintPath>..\Shared\Managed\ItemComponent.Types.dll</HintPath> <HintPath>..\Shared\Managed\ItemComponent.Types.dll</HintPath>
</Reference> </Reference>
<Reference Include="Mono.Cecil, Version=0.11.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Mdb, Version=0.11.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Mdb.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Pdb, Version=0.11.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Pdb.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Rocks, Version=0.11.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Rocks.dll</HintPath>
</Reference>
<Reference Include="MonoMod.RuntimeDetour, Version=21.12.13.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MonoMod.RuntimeDetour.21.12.13.1\lib\net452\MonoMod.RuntimeDetour.dll</HintPath>
</Reference>
<Reference Include="MonoMod.Utils, Version=21.12.13.1, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MonoMod.Utils.21.12.13.1\lib\net452\MonoMod.Utils.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="UnityEngine"> <Reference Include="UnityEngine">
<HintPath>..\Shared\Managed\UnityEngine.dll</HintPath> <HintPath>..\Shared\Managed\UnityEngine.dll</HintPath>
@ -76,4 +99,11 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\BepInEx.Core.5.4.19\build\BepInEx.Core.targets" Condition="Exists('..\packages\BepInEx.Core.5.4.19\build\BepInEx.Core.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\BepInEx.Core.5.4.19\build\BepInEx.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\BepInEx.Core.5.4.19\build\BepInEx.Core.targets'))" />
</Target>
</Project> </Project>

View File

@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="BepInEx.BaseLib" version="5.4.17" targetFramework="net472" /> <package id="BepInEx.BaseLib" version="5.4.19" targetFramework="net472" />
<package id="BepInEx.Core" version="5.4.19" targetFramework="net472" />
<package id="HarmonyX" version="2.7.0" targetFramework="net472" />
<package id="Mono.Cecil" version="0.11.4" targetFramework="net472" />
<package id="MonoMod.RuntimeDetour" version="21.12.13.1" targetFramework="net472" />
<package id="MonoMod.Utils" version="21.12.13.1" targetFramework="net472" />
</packages> </packages>