using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using GBFreeCam;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(FreeCamMod), "FreecamMod", "1.1.0", "Mel", "Detachable free camera for Gang Beasts. Multiplayer-safe.")]
[assembly: MelonGame("Boneloaf", "Gang Beasts")]
[assembly: AssemblyCompany("FreecamMod")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0")]
[assembly: AssemblyProduct("FreecamMod")]
[assembly: AssemblyTitle("FreecamMod")]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.1.0.0")]
[module: UnverifiableCode]
namespace GBFreeCam;
public class FreeCamMod : MelonMod
{
private const float BaseSpeed = 12f;
private const float FastMultiplier = 5f;
private const float SlowMultiplier = 0.2f;
private const float LookSensitivity = 0.15f;
private const float ZoomSpeed = 60f;
private const float MinFov = 10f;
private const float MaxFov = 170f;
private const float MinPitch = -89f;
private const float MaxPitch = 89f;
private const float SlowMotionScale = 0.25f;
private const int MaxErrors = 8;
private const float MultiplayerPollInterval = 1f;
private const float SceneSettleDelay = 0.35f;
private static readonly string[] HudSkipKeywords = new string[28]
{
"lobby", "party", "invite", "friend", "network", "netcode", "session", "host", "menu", "pause",
"settings", "option", "loading", "credit", "popup", "tooltip", "tutorial", "keyboard", "controller", "error",
"disconnect", "kick", "chat", "steam", "overlay", "notify", "notification", "cursor"
};
private static readonly string[] PlayerIdMarkers = new string[3] { "PlayerID:", "Player ID:", "PlayerId:" };
private bool active;
private bool selfDisabled;
private int errorCount;
private GameObject camObject;
private Camera freeCam;
private readonly List<Camera> suppressedCams = new List<Camera>();
private float yaw;
private float pitch;
private bool frozen;
private bool slowMotion;
private bool timeOverridden;
private float savedTimeScale = 1f;
private bool hudHidden;
private readonly List<Canvas> hiddenCanvases = new List<Canvas>();
private readonly List<bool> hiddenCanvasPrevState = new List<bool>();
private bool inMultiplayer;
private bool multiplayerKnown;
private bool warnedTimeInMultiplayer;
private bool warnedHudSkip;
private float multiplayerPollTimer;
private float sceneSettleTimer;
private bool cursorWasLocked;
public override void OnInitializeMelon()
{
MelonLogger.Msg("[FreecamMod] loaded. F1 freecam | F2 gameplay HUD | F3 freeze | F4 slow-mo | F5 ortho | R reset view");
}
public override void OnUpdate()
{
if (selfDisabled)
{
return;
}
try
{
OnUpdateSafe();
}
catch (Exception ex)
{
HandleFailure("OnUpdate", ex);
}
}
private void OnUpdateSafe()
{
Keyboard current = Keyboard.current;
if (current == null)
{
return;
}
if (sceneSettleTimer > 0f)
{
sceneSettleTimer -= Time.unscaledDeltaTime;
}
multiplayerPollTimer -= Time.unscaledDeltaTime;
if (multiplayerPollTimer <= 0f)
{
multiplayerPollTimer = 1f;
RefreshMultiplayerState();
}
if (((ButtonControl)current.f1Key).wasPressedThisFrame)
{
ToggleFreeCam();
}
if (((ButtonControl)current.f2Key).wasPressedThisFrame)
{
ToggleHud();
}
if (((ButtonControl)current.f3Key).wasPressedThisFrame)
{
ToggleFreeze();
}
if (((ButtonControl)current.f4Key).wasPressedThisFrame)
{
ToggleSlowMotion();
}
ApplyTimeOverride();
if (active && !((Object)(object)freeCam == (Object)null))
{
MaintainCameraSuppression();
if (((ButtonControl)current.f5Key).wasPressedThisFrame)
{
ToggleOrthographic();
}
if (((ButtonControl)current.rKey).wasPressedThisFrame)
{
ResetView();
}
UpdateMovement(current);
UpdateLook();
UpdateZoom(current);
}
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
if (selfDisabled)
{
return;
}
try
{
sceneSettleTimer = 0.35f;
multiplayerPollTimer = 0f;
multiplayerKnown = false;
ReleaseTimeOverride();
if (active)
{
RestoreSuppressedCameras();
ClearDestroyedSuppressedCameras();
if ((Object)(object)freeCam == (Object)null || (Object)(object)camObject == (Object)null)
{
MelonLogger.Warning("[FreecamMod] free camera lost on scene load, rebuilding.");
active = false;
ToggleFreeCam();
}
sceneSettleTimer = 0.35f;
}
}
catch (Exception ex)
{
HandleFailure("OnSceneWasLoaded", ex);
}
}
public override void OnApplicationQuit()
{
Shutdown();
}
private void HandleFailure(string where, Exception ex)
{
errorCount++;
MelonLogger.Error($"[FreecamMod] {where} failed ({errorCount}/{8}): {ex.GetType().Name}: {ex.Message}");
if (errorCount >= 8)
{
MelonLogger.Error("[FreecamMod] too many failures - disabling the mod and restoring the game to its original state.");
Shutdown();
selfDisabled = true;
}
}
private void Shutdown()
{
try
{
ReleaseTimeOverride();
RestoreHud();
DisableCamera();
RestoreCursor();
}
catch (Exception ex)
{
MelonLogger.Error("[FreecamMod] shutdown error: " + ex.Message);
}
}
private void UpdateMovement(Keyboard kb)
{
//IL_003c: Unknown result type (might be due to invalid IL or missing references)
//IL_0041: Unknown result type (might be due to invalid IL or missing references)
//IL_004f: Unknown result type (might be due to invalid IL or missing references)
//IL_0051: Unknown result type (might be due to invalid IL or missing references)
//IL_0056: Unknown result type (might be due to invalid IL or missing references)
//IL_005b: Unknown result type (might be due to invalid IL or missing references)
//IL_0069: Unknown result type (might be due to invalid IL or missing references)
//IL_006b: Unknown result type (might be due to invalid IL or missing references)
//IL_0070: Unknown result type (might be due to invalid IL or missing references)
//IL_0075: Unknown result type (might be due to invalid IL or missing references)
//IL_0083: Unknown result type (might be due to invalid IL or missing references)
//IL_0085: Unknown result type (might be due to invalid IL or missing references)
//IL_008a: Unknown result type (might be due to invalid IL or missing references)
//IL_008f: Unknown result type (might be due to invalid IL or missing references)
//IL_009d: Unknown result type (might be due to invalid IL or missing references)
//IL_009f: Unknown result type (might be due to invalid IL or missing references)
//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
//IL_00d1: Unknown result type (might be due to invalid IL or missing references)
//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
//IL_00db: Unknown result type (might be due to invalid IL or missing references)
//IL_00ec: Unknown result type (might be due to invalid IL or missing references)
//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
//IL_0104: Unknown result type (might be due to invalid IL or missing references)
float num = 12f;
if (((ButtonControl)kb.leftShiftKey).isPressed)
{
num *= 5f;
}
if (((ButtonControl)kb.leftCtrlKey).isPressed)
{
num *= 0.2f;
}
Transform transform = ((Component)freeCam).transform;
Vector3 val = Vector3.zero;
if (((ButtonControl)kb.upArrowKey).isPressed)
{
val += transform.forward;
}
if (((ButtonControl)kb.downArrowKey).isPressed)
{
val -= transform.forward;
}
if (((ButtonControl)kb.rightArrowKey).isPressed)
{
val += transform.right;
}
if (((ButtonControl)kb.leftArrowKey).isPressed)
{
val -= transform.right;
}
if (((ButtonControl)kb.eKey).isPressed)
{
val += Vector3.up;
}
if (((ButtonControl)kb.qKey).isPressed)
{
val -= Vector3.up;
}
if (((Vector3)(ref val)).sqrMagnitude > 0f)
{
transform.position += ((Vector3)(ref val)).normalized * (num * Time.unscaledDeltaTime);
}
}
private void UpdateLook()
{
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_002c: Unknown result type (might be due to invalid IL or missing references)
//IL_0045: Unknown result type (might be due to invalid IL or missing references)
//IL_0082: Unknown result type (might be due to invalid IL or missing references)
Mouse current = Mouse.current;
if (current != null)
{
Vector2 val = ((InputControl<Vector2>)(object)((Pointer)current).delta).ReadValue();
if (!(((Vector2)(ref val)).sqrMagnitude <= 0f))
{
yaw += val.x * 0.15f;
pitch = Mathf.Clamp(pitch - val.y * 0.15f, -89f, 89f);
((Component)freeCam).transform.rotation = Quaternion.Euler(pitch, yaw, 0f);
}
}
}
private void UpdateZoom(Keyboard kb)
{
float num = 0f;
if (((ButtonControl)kb.equalsKey).isPressed || ((ButtonControl)kb.numpadPlusKey).isPressed)
{
num += 60f * Time.unscaledDeltaTime;
}
if (((ButtonControl)kb.minusKey).isPressed || ((ButtonControl)kb.numpadMinusKey).isPressed)
{
num -= 60f * Time.unscaledDeltaTime;
}
if (!Mathf.Approximately(num, 0f))
{
if (freeCam.orthographic)
{
freeCam.orthographicSize = Mathf.Clamp(freeCam.orthographicSize - num, 0.1f, 100f);
}
else
{
freeCam.fieldOfView = Mathf.Clamp(freeCam.fieldOfView + num, 10f, 170f);
}
}
}
private void ToggleFreeCam()
{
//IL_003f: Unknown result type (might be due to invalid IL or missing references)
//IL_0049: Expected O, but got Unknown
//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
//IL_00c7: Unknown result type (might be due to invalid IL or missing references)
//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
//IL_00ce: Unknown result type (might be due to invalid IL or missing references)
//IL_00da: Unknown result type (might be due to invalid IL or missing references)
if (active)
{
DisableCamera();
RestoreCursor();
MelonLogger.Msg("[FreecamMod] free camera off.");
return;
}
Camera val = SafeGetMainCamera();
if ((Object)(object)val == (Object)null)
{
MelonLogger.Warning("[FreecamMod] no main camera yet - try again in a moment.");
return;
}
camObject = new GameObject("FreecamMod_Camera");
Object.DontDestroyOnLoad((Object)(object)camObject);
freeCam = camObject.AddComponent<Camera>();
freeCam.CopyFrom(val);
UniversalAdditionalCameraData val2 = default(UniversalAdditionalCameraData);
if (!camObject.TryGetComponent<UniversalAdditionalCameraData>(ref val2))
{
val2 = camObject.AddComponent<UniversalAdditionalCameraData>();
}
val2.renderPostProcessing = true;
Transform transform = ((Component)val).transform;
((Component)freeCam).transform.position = transform.position;
((Component)freeCam).transform.rotation = transform.rotation;
Vector3 eulerAngles = transform.eulerAngles;
pitch = eulerAngles.x;
yaw = eulerAngles.y;
((Behaviour)freeCam).enabled = true;
SuppressCamera(val);
LockCursor();
active = true;
MelonLogger.Msg("[FreecamMod] free camera on.");
}
private void SuppressCamera(Camera cam)
{
if ((Object)(object)cam == (Object)null || (Object)(object)cam == (Object)(object)freeCam)
{
return;
}
for (int i = 0; i < suppressedCams.Count; i++)
{
if ((Object)(object)suppressedCams[i] == (Object)(object)cam)
{
return;
}
}
if (((Behaviour)cam).enabled)
{
((Behaviour)cam).enabled = false;
suppressedCams.Add(cam);
}
}
private void RestoreSuppressedCameras()
{
for (int i = 0; i < suppressedCams.Count; i++)
{
Camera val = suppressedCams[i];
if (!((Object)(object)val == (Object)null))
{
try
{
((Behaviour)val).enabled = true;
}
catch (Exception ex)
{
MelonLogger.Error("[FreecamMod] could not re-enable camera: " + ex.Message);
}
}
}
suppressedCams.Clear();
}
private void ClearDestroyedSuppressedCameras()
{
for (int num = suppressedCams.Count - 1; num >= 0; num--)
{
if ((Object)(object)suppressedCams[num] == (Object)null)
{
suppressedCams.RemoveAt(num);
}
}
}
private void DisableCamera()
{
RestoreSuppressedCameras();
if ((Object)(object)camObject != (Object)null)
{
Object.Destroy((Object)(object)camObject);
}
camObject = null;
freeCam = null;
active = false;
}
private static Camera SafeGetMainCamera()
{
try
{
return Camera.main;
}
catch (Exception)
{
return null;
}
}
private void MaintainCameraSuppression()
{
if (!active || sceneSettleTimer > 0f)
{
return;
}
ClearDestroyedSuppressedCameras();
if (suppressedCams.Count <= 0)
{
Camera val = SafeGetMainCamera();
if ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)freeCam)
{
SuppressCamera(val);
}
}
}
private void ResetView()
{
//IL_0078: Unknown result type (might be due to invalid IL or missing references)
//IL_008e: Unknown result type (might be due to invalid IL or missing references)
//IL_0099: Unknown result type (might be due to invalid IL or missing references)
//IL_009e: Unknown result type (might be due to invalid IL or missing references)
//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)freeCam == (Object)null)
{
return;
}
Camera val = null;
for (int i = 0; i < suppressedCams.Count; i++)
{
if ((Object)(object)suppressedCams[i] != (Object)null)
{
val = suppressedCams[i];
break;
}
}
val = (((Object)(object)val != (Object)null) ? val : SafeGetMainCamera());
if ((Object)(object)val != (Object)null)
{
Transform transform = ((Component)val).transform;
((Component)freeCam).transform.position = transform.position;
((Component)freeCam).transform.rotation = transform.rotation;
Vector3 eulerAngles = transform.eulerAngles;
pitch = eulerAngles.x;
yaw = eulerAngles.y;
}
if (!freeCam.orthographic)
{
freeCam.fieldOfView = 60f;
}
}
private void ToggleOrthographic()
{
if (!((Object)(object)freeCam == (Object)null))
{
freeCam.orthographic = !freeCam.orthographic;
if (freeCam.orthographic)
{
freeCam.orthographicSize = 5f;
}
else
{
freeCam.fieldOfView = 60f;
}
}
}
private void LockCursor()
{
Cursor.lockState = (CursorLockMode)1;
Cursor.visible = false;
cursorWasLocked = true;
}
private void RestoreCursor()
{
if (cursorWasLocked)
{
cursorWasLocked = false;
Cursor.lockState = (CursorLockMode)0;
Cursor.visible = true;
}
}
private void ToggleHud()
{
if (hudHidden)
{
RestoreHud();
MelonLogger.Msg("[FreecamMod] gameplay HUD shown.");
return;
}
Il2CppArrayBase<Canvas> val = Object.FindObjectsOfType<Canvas>(true);
if (val == null)
{
MelonLogger.Warning("[FreecamMod] no HUD found to hide.");
return;
}
StringBuilder stringBuilder = new StringBuilder();
int num = 0;
for (int i = 0; i < val.Length; i++)
{
Canvas val2 = val[i];
if ((Object)(object)val2 == (Object)null)
{
continue;
}
GameObject gameObject = ((Component)val2).gameObject;
if ((Object)(object)gameObject == (Object)null || !gameObject.activeSelf)
{
continue;
}
if (IsProtectedHud(((Object)gameObject).name))
{
if (stringBuilder.Length < 400)
{
stringBuilder.Append(((Object)gameObject).name).Append(", ");
}
continue;
}
try
{
hiddenCanvases.Add(val2);
hiddenCanvasPrevState.Add(item: true);
gameObject.SetActive(false);
num++;
}
catch (Exception ex)
{
MelonLogger.Error("[FreecamMod] could not hide canvas '" + ((Object)gameObject).name + "': " + ex.Message);
}
}
hudHidden = true;
MelonLogger.Msg($"[FreecamMod] gameplay HUD hidden ({num} canvas(es)).");
if (stringBuilder.Length > 0 && !warnedHudSkip)
{
warnedHudSkip = true;
MelonLogger.Msg("[FreecamMod] left lobby/menu UI alone: " + stringBuilder.ToString().TrimEnd(',', ' '));
}
}
private void RestoreHud()
{
for (int i = 0; i < hiddenCanvases.Count; i++)
{
Canvas val = hiddenCanvases[i];
if (!((Object)(object)val == (Object)null))
{
try
{
((Component)val).gameObject.SetActive(hiddenCanvasPrevState[i]);
}
catch (Exception ex)
{
MelonLogger.Error("[FreecamMod] could not restore canvas: " + ex.Message);
}
}
}
hiddenCanvases.Clear();
hiddenCanvasPrevState.Clear();
hudHidden = false;
}
private static bool IsProtectedHud(string name)
{
if (string.IsNullOrEmpty(name))
{
return true;
}
for (int i = 0; i < HudSkipKeywords.Length; i++)
{
if (name.IndexOf(HudSkipKeywords[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private void ToggleFreeze()
{
if (CanUseTimeControls())
{
frozen = !frozen;
if (!frozen)
{
slowMotion = false;
}
ApplyTimeOverride();
MelonLogger.Msg(frozen ? "[FreecamMod] time frozen." : "[FreecamMod] time resumed.");
}
}
private void ToggleSlowMotion()
{
if (CanUseTimeControls())
{
if (frozen)
{
MelonLogger.Warning("[FreecamMod] resume time (F3) before using slow motion.");
return;
}
slowMotion = !slowMotion;
ApplyTimeOverride();
MelonLogger.Msg(slowMotion ? "[FreecamMod] slow motion on." : "[FreecamMod] slow motion off.");
}
}
private bool CanUseTimeControls()
{
if (inMultiplayer)
{
if (!warnedTimeInMultiplayer)
{
warnedTimeInMultiplayer = true;
MelonLogger.Warning("[FreecamMod] freeze/slow-mo are disabled while in a multiplayer session (they change the global time scale).");
}
return false;
}
return true;
}
private void ApplyTimeOverride()
{
if (!frozen && !slowMotion)
{
ReleaseTimeOverride();
return;
}
float timeScale = (frozen ? 0f : 0.25f);
if (!timeOverridden)
{
if (Time.timeScale > 0f)
{
savedTimeScale = Time.timeScale;
}
timeOverridden = true;
}
Time.timeScale = timeScale;
}
private void ReleaseTimeOverride()
{
if (timeOverridden)
{
timeOverridden = false;
frozen = false;
slowMotion = false;
if (Time.timeScale <= 0.251f)
{
Time.timeScale = ((savedTimeScale > 0f) ? savedTimeScale : 1f);
}
}
}
private void RefreshMultiplayerState()
{
//IL_0002: Unknown result type (might be due to invalid IL or missing references)
//IL_0007: Unknown result type (might be due to invalid IL or missing references)
bool flag = false;
try
{
Scene activeScene = SceneManager.GetActiveScene();
if (!((Scene)(ref activeScene)).IsValid() || !((Scene)(ref activeScene)).isLoaded)
{
return;
}
Il2CppArrayBase<GameObject> rootGameObjects = (Il2CppArrayBase<GameObject>)(object)((Scene)(ref activeScene)).GetRootGameObjects();
if (rootGameObjects == null)
{
return;
}
HashSet<int> hashSet = new HashSet<int>();
for (int i = 0; i < rootGameObjects.Length; i++)
{
GameObject val = rootGameObjects[i];
if ((Object)(object)val == (Object)null)
{
continue;
}
int num = ParsePlayerId(((Object)val).name);
if (num >= 0)
{
hashSet.Add(num);
if (num > 0)
{
flag = true;
}
}
}
if (hashSet.Count > 1)
{
flag = true;
}
}
catch (Exception ex)
{
if (!multiplayerKnown)
{
MelonLogger.Error("[FreecamMod] multiplayer detection failed: " + ex.Message);
}
return;
}
if (multiplayerKnown && flag != inMultiplayer)
{
MelonLogger.Msg(flag ? "[FreecamMod] multiplayer session detected - time controls disabled." : "[FreecamMod] left multiplayer session - time controls available again.");
if (flag)
{
ReleaseTimeOverride();
}
}
inMultiplayer = flag;
multiplayerKnown = true;
}
private static int ParsePlayerId(string name)
{
if (string.IsNullOrEmpty(name))
{
return -1;
}
for (int i = 0; i < PlayerIdMarkers.Length; i++)
{
int num = name.IndexOf(PlayerIdMarkers[i], StringComparison.OrdinalIgnoreCase);
if (num >= 0)
{
int j;
for (j = num + PlayerIdMarkers[i].Length; j < name.Length && name[j] == ' '; j++)
{
}
int k;
for (k = j; k < name.Length && char.IsDigit(name[k]); k++)
{
}
if (k > j && int.TryParse(name.Substring(j, k - j), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
}
}
return -1;
}
}