diff --git a/GorillaTagModMenu/GorillaTagModMenu.csproj b/GorillaTagModMenu/GorillaTagModMenu.csproj new file mode 100644 index 0000000..044a4a8 --- /dev/null +++ b/GorillaTagModMenu/GorillaTagModMenu.csproj @@ -0,0 +1,28 @@ + + + netstandard2.1 + latest + enable + disable + ReadonsMenu + ReadonsMenu + + + + + diff --git a/GorillaTagModMenu/Plugin.cs b/GorillaTagModMenu/Plugin.cs new file mode 100644 index 0000000..7993bdd --- /dev/null +++ b/GorillaTagModMenu/Plugin.cs @@ -0,0 +1,375 @@ +using System.Collections.Generic; +using BepInEx; +using Photon.Pun; +using Photon.Realtime; +using UnityEngine; +using UnityEngine.XR; + +namespace GorillaTagModMenu; + +[BepInPlugin(PluginGuid, PluginName, PluginVersion)] +public sealed class Plugin : BaseUnityPlugin +{ + public const string PluginGuid = "com.readonsmenu.gorillatag.modmenu"; + public const string PluginName = "Readons Gorilla Tag Menu"; + public const string PluginVersion = "1.1.0"; + + private enum MenuAction + { + ToggleNoclip, + ToggleSpeed, + ToggleKickGun, + IncreaseSpeed, + DecreaseSpeed + } + + private readonly Dictionary vrButtonActions = new(); + private Rect windowRect = new(20f, 20f, 320f, 220f); + + private bool showMenu; + private bool noclipEnabled; + private bool speedBoostEnabled; + private bool kickGunEnabled; + + private bool wasPrimaryButtonPressed; + private bool wasTriggerPressed; + private bool triggerPressedThisFrame; + + private float moveSpeed = 9f; + + private GameObject menuRoot; + private Transform buttonContainer; + + private void Update() + { + UpdateVrTriggerState(); + HandleVrMenuToggle(); + HandleVrMenuInteraction(); + + if (Input.GetKeyDown(KeyCode.Insert)) + { + showMenu = !showMenu; + SetVrMenuActive(showMenu); + } + + if (showMenu && menuRoot != null) + { + UpdateVrButtonLabels(); + } + + HandleNoclip(); + HandleSpeedBoost(); + HandleKickGun(); + } + + private void OnGUI() + { + if (!showMenu) + { + return; + } + + windowRect = GUI.Window(7331, windowRect, DrawWindow, $"{PluginName} v{PluginVersion}"); + } + + private void DrawWindow(int id) + { + GUILayout.BeginVertical(); + noclipEnabled = GUILayout.Toggle(noclipEnabled, "Noclip"); + speedBoostEnabled = GUILayout.Toggle(speedBoostEnabled, "Speed Boost"); + kickGunEnabled = GUILayout.Toggle(kickGunEnabled, "Kick Gun"); + + GUILayout.Space(10f); + GUILayout.Label($"Move Speed: {moveSpeed:F1}"); + moveSpeed = GUILayout.HorizontalSlider(moveSpeed, 5f, 18f); + + GUILayout.Space(12f); + GUILayout.Label("Insert or Right A Button = Toggle menu"); + GUILayout.Label("VR Trigger = Press menu buttons / Kick Gun trigger"); + GUILayout.EndVertical(); + + GUI.DragWindow(new Rect(0f, 0f, 10000f, 24f)); + } + + private void UpdateVrTriggerState() + { + triggerPressedThisFrame = false; + + var rightHand = InputDevices.GetDeviceAtXRNode(XRNode.RightHand); + if (!rightHand.isValid) + { + wasTriggerPressed = false; + return; + } + + rightHand.TryGetFeatureValue(CommonUsages.triggerButton, out var triggerPressed); + triggerPressedThisFrame = triggerPressed && !wasTriggerPressed; + wasTriggerPressed = triggerPressed; + } + + private void HandleVrMenuToggle() + { + var rightHand = InputDevices.GetDeviceAtXRNode(XRNode.RightHand); + if (!rightHand.isValid) + { + return; + } + + rightHand.TryGetFeatureValue(CommonUsages.primaryButton, out var primaryButtonPressed); + + if (primaryButtonPressed && !wasPrimaryButtonPressed) + { + showMenu = !showMenu; + EnsureVrMenu(); + PositionMenuInFrontOfPlayer(); + SetVrMenuActive(showMenu); + } + + wasPrimaryButtonPressed = primaryButtonPressed; + } + + private void HandleVrMenuInteraction() + { + if (!showMenu) + { + return; + } + + var rightHand = InputDevices.GetDeviceAtXRNode(XRNode.RightHand); + if (!rightHand.isValid || !triggerPressedThisFrame) + { + return; + } + + if (rightHand.TryGetFeatureValue(CommonUsages.devicePosition, out var position) && + rightHand.TryGetFeatureValue(CommonUsages.deviceRotation, out var rotation)) + { + var ray = new Ray(position, rotation * Vector3.forward); + if (Physics.Raycast(ray, out var hit, 6f) && vrButtonActions.TryGetValue(hit.collider, out var action)) + { + ApplyMenuAction(action); + } + } + } + + private void ApplyMenuAction(MenuAction action) + { + switch (action) + { + case MenuAction.ToggleNoclip: + noclipEnabled = !noclipEnabled; + break; + case MenuAction.ToggleSpeed: + speedBoostEnabled = !speedBoostEnabled; + break; + case MenuAction.ToggleKickGun: + kickGunEnabled = !kickGunEnabled; + break; + case MenuAction.IncreaseSpeed: + moveSpeed = Mathf.Clamp(moveSpeed + 1f, 5f, 18f); + break; + case MenuAction.DecreaseSpeed: + moveSpeed = Mathf.Clamp(moveSpeed - 1f, 5f, 18f); + break; + } + + UpdateVrButtonLabels(); + } + + private void EnsureVrMenu() + { + if (menuRoot != null) + { + return; + } + + menuRoot = new GameObject("ReadonsVrMenu"); + var panel = GameObject.CreatePrimitive(PrimitiveType.Cube); + panel.transform.SetParent(menuRoot.transform, false); + panel.transform.localScale = new Vector3(0.3f, 0.4f, 0.01f); + panel.GetComponent().material.color = new Color(0.06f, 0.06f, 0.06f, 0.85f); + + buttonContainer = new GameObject("Buttons").transform; + buttonContainer.SetParent(menuRoot.transform, false); + + CreateVrButton(new Vector3(0f, 0.13f, -0.01f), MenuAction.ToggleNoclip, "Noclip: OFF"); + CreateVrButton(new Vector3(0f, 0.05f, -0.01f), MenuAction.ToggleSpeed, "Speed: OFF"); + CreateVrButton(new Vector3(0f, -0.03f, -0.01f), MenuAction.ToggleKickGun, "Kick Gun: OFF"); + CreateVrButton(new Vector3(-0.08f, -0.11f, -0.01f), MenuAction.DecreaseSpeed, "Speed -"); + CreateVrButton(new Vector3(0.08f, -0.11f, -0.01f), MenuAction.IncreaseSpeed, "Speed +"); + + SetVrMenuActive(false); + } + + private void CreateVrButton(Vector3 localPosition, MenuAction action, string text) + { + var button = GameObject.CreatePrimitive(PrimitiveType.Cube); + button.transform.SetParent(buttonContainer, false); + button.transform.localPosition = localPosition; + button.transform.localScale = new Vector3(0.22f, 0.06f, 0.02f); + button.GetComponent().material.color = new Color(0.15f, 0.15f, 0.15f, 1f); + + var textObj = new GameObject("Label"); + textObj.transform.SetParent(button.transform, false); + textObj.transform.localPosition = new Vector3(0f, 0f, -0.013f); + + var textMesh = textObj.AddComponent(); + textMesh.text = text; + textMesh.fontSize = 60; + textMesh.characterSize = 0.003f; + textMesh.anchor = TextAnchor.MiddleCenter; + textMesh.alignment = TextAlignment.Center; + textMesh.color = Color.white; + + if (button.TryGetComponent(out var collider)) + { + vrButtonActions[collider] = action; + } + } + + private void UpdateVrButtonLabels() + { + if (buttonContainer == null) + { + return; + } + + for (var i = 0; i < buttonContainer.childCount; i++) + { + var button = buttonContainer.GetChild(i); + var label = button.GetComponentInChildren(); + var collider = button.GetComponent(); + if (label == null || collider == null || !vrButtonActions.TryGetValue(collider, out var action)) + { + continue; + } + + switch (action) + { + case MenuAction.ToggleNoclip: + label.text = $"Noclip: {(noclipEnabled ? "ON" : "OFF")}"; + break; + case MenuAction.ToggleSpeed: + label.text = $"Speed: {(speedBoostEnabled ? "ON" : "OFF")}"; + break; + case MenuAction.ToggleKickGun: + label.text = $"Kick Gun: {(kickGunEnabled ? "ON" : "OFF")}"; + break; + case MenuAction.IncreaseSpeed: + label.text = $"Speed + ({moveSpeed:F1})"; + break; + case MenuAction.DecreaseSpeed: + label.text = $"Speed - ({moveSpeed:F1})"; + break; + } + } + } + + private void PositionMenuInFrontOfPlayer() + { + if (Camera.main == null || menuRoot == null) + { + return; + } + + var cameraTransform = Camera.main.transform; + var flatForward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up).normalized; + if (flatForward.sqrMagnitude <= 0.001f) + { + flatForward = cameraTransform.forward; + } + + menuRoot.transform.position = cameraTransform.position + flatForward * 0.6f; + menuRoot.transform.rotation = Quaternion.LookRotation(-flatForward, Vector3.up); + } + + private void SetVrMenuActive(bool active) + { + EnsureVrMenu(); + menuRoot.SetActive(active); + } + + private void HandleNoclip() + { + if (Camera.main == null) + { + return; + } + + var localPlayer = Camera.main.transform.root; + var colliders = localPlayer.GetComponentsInChildren(false); + foreach (var col in colliders) + { + col.enabled = !noclipEnabled; + } + } + + private void HandleSpeedBoost() + { + if (!speedBoostEnabled || Camera.main == null) + { + return; + } + + var transformRef = Camera.main.transform; + var direction = Vector3.zero; + + if (Input.GetKey(KeyCode.W)) direction += transformRef.forward; + if (Input.GetKey(KeyCode.S)) direction -= transformRef.forward; + if (Input.GetKey(KeyCode.D)) direction += transformRef.right; + if (Input.GetKey(KeyCode.A)) direction -= transformRef.right; + + direction.y = 0f; + if (direction.sqrMagnitude > 0f) + { + transformRef.root.position += direction.normalized * (moveSpeed * Time.deltaTime); + } + } + + private void HandleKickGun() + { + if (!kickGunEnabled || Camera.main == null) + { + return; + } + + var mouseTrigger = Input.GetMouseButtonDown(0); + var vrTrigger = triggerPressedThisFrame; + if (!mouseTrigger && !vrTrigger) + { + return; + } + + var ray = new Ray(Camera.main.transform.position, Camera.main.transform.forward); + if (!Physics.Raycast(ray, out var hitInfo, 500f)) + { + return; + } + + var targetView = hitInfo.collider.GetComponentInParent(); + if (targetView?.Owner == null) + { + return; + } + + TryKickPlayer(targetView.Owner); + } + + private void TryKickPlayer(Player target) + { + if (!PhotonNetwork.IsMasterClient) + { + Logger.LogWarning("Kick Gun requires Master Client."); + return; + } + + if (target.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber) + { + Logger.LogInfo("Refusing to kick local player."); + return; + } + + PhotonNetwork.CloseConnection(target); + Logger.LogInfo($"Kick attempted for {target.NickName} (#{target.ActorNumber})."); + } +} diff --git a/README.md b/README.md index 4bd8fd2..4d90316 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ -# Readonsmenu -This is a mod menu for the Virtual realtiy game Gorilla tag +# Gorilla Tag Mod Menu (BepInEx) + +This repository contains source code for a Gorilla Tag BepInEx plugin project. + +> Note: I can't generate a compiled `.dll` binary in this environment because the .NET SDK is not installed. The project is configured so that when you build locally, the output file name is `ReadonsMenu.dll`. + +## Files +- `GorillaTagModMenu/GorillaTagModMenu.csproj` +- `GorillaTagModMenu/Plugin.cs` + +## Build to DLL (on your PC) +1. Install .NET SDK 8.0+ and BepInEx. +2. Edit `GorillaTagModMenu/GorillaTagModMenu.csproj` and add `` paths for your local game DLLs. +3. Build: + ```bash + dotnet build GorillaTagModMenu/GorillaTagModMenu.csproj -c Release + ``` +4. Your output DLL will be at: + - `GorillaTagModMenu/bin/Release/netstandard2.1/ReadonsMenu.dll` +5. Copy that DLL into your Gorilla Tag `BepInEx/plugins` folder. + +A `.dll` is a compiled binary file, so source code from `README.md` / `.csproj` cannot literally be embedded as plain text in one file. The correct way is to keep source files in the project and compile them into `ReadonsMenu.dll`.