C#:
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("HighJump", "NINJA WORKS", "1.0.2")]
public class HighJump : RustPlugin
{
#region Constants
private const bool DEBUG_MODE = false;
private const float DOUBLE_JUMP_WINDOW = 0.4f;
private const float GROUND_CHECK_DISTANCE = 0.5f;
private const float JUMP_COOLDOWN = 0.3f;
private const float CEILING_CHECK_DISTANCE = 2.0f;
private const float GRAVITY = 20f;
private const float CEILING_CHECK_RADIUS = 0.3f;
private const float CEILING_CHECK_MARGIN = 0.5f;
private const float CEILING_SAFE_MARGIN = 0.2f;
private const float PLAYER_HEAD_OFFSET = 1.5f;
private const float STUCK_CHECK_HEIGHT = 0.9f;
private const float STUCK_CHECK_RADIUS = 0.4f;
private const float SAFE_POS_RAY_START = 2f;
private const float SAFE_POS_RAY_DISTANCE = 10f;
private const float GROUND_OFFSET = 0.1f;
private const float BOOSTER_SPAWN_HEIGHT = 500f;
private const float FALL_DAMAGE_IMMUNITY_DURATION = 5f;
private const float MAX_SAFE_FALL_HEIGHT = 20f;
private const string PERM_USE = "HighJump.use";
private const string PERM_VIP1 = "HighJump.vip1";
private const string PERM_VIP2 = "HighJump.vip2";
private const string PERM_VIP3 = "HighJump.vip3";
private static int _physicsLayerMask = -1;
private static int PhysicsLayerMask
{
get
{
if (_physicsLayerMask == -1)
{
_physicsLayerMask = LayerMask.GetMask("Terrain", "World", "Construction", "Default");
}
return _physicsLayerMask;
}
}
#endregion
#region Configuration
private ConfigData config;
private class ConfigData
{
[JsonProperty("Double Jump Calorie Cost")]
public float DoubleJumpCalorieCost { get; set; } = 1f;
[JsonProperty("Jump Force - Use Permission")]
public float JumpForceUse { get; set; } = 14f;
[JsonProperty("Jump Force - VIP1 Permission")]
public float JumpForceVip1 { get; set; } = 17f;
[JsonProperty("Jump Force - VIP2 Permission")]
public float JumpForceVip2 { get; set; } = 20f;
[JsonProperty("Jump Force - VIP3 Permission")]
public float JumpForceVip3 { get; set; } = 23f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<ConfigData>();
if (config == null)
{
LoadDefaultConfig();
}
}
catch
{
LoadDefaultConfig();
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
config = new ConfigData();
}
protected override void SaveConfig()
{
Config.WriteObject(config);
}
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoData"] = "<color=#ff6666>No position data available.</color>",
["JumpingCannotUse"] = "<color=#ff6666>Cannot use while jumping.</color>",
["NoValidPosition"] = "<color=#ff6666>No resettable position available. Use double jump first.</color>",
["PositionReset"] = "<color=#66ff66>Returned to position before double jump.</color>"
}, this, "en");
}
private string Lang(string key, string userId = null) => lang.GetMessage(key, this, userId);
#endregion
#region Data
private class JumpData
{
public float JumpTime;
public bool CanDoubleJump;
public float LastDoubleJumpTime;
public bool IsJumping;
public float DoubleJumpTime;
public float DoubleJumpStartHeight;
public float DoubleJumpPeakHeight;
public Vector3 PreJumpPosition;
public bool HasValidPreJumpPosition;
public bool FallDamageImmune;
public bool HasPermission;
public float JumpForce;
}
private Dictionary<ulong, JumpData> playerJumpData = new Dictionary<ulong, JumpData>();
private HashSet<JumpBooster> activeJumpBoosters = new HashSet<JumpBooster>();
private HashSet<string> pluginPermissions = new HashSet<string> { PERM_USE, PERM_VIP1, PERM_VIP2, PERM_VIP3 };
private HashSet<ulong> jumpButtonHeld = new HashSet<ulong>();
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PERM_USE, this);
permission.RegisterPermission(PERM_VIP1, this);
permission.RegisterPermission(PERM_VIP2, this);
permission.RegisterPermission(PERM_VIP3, this);
}
private void Unload()
{
foreach (var booster in activeJumpBoosters.ToArray())
{
if (booster != null)
{
booster.Cleanup();
}
}
activeJumpBoosters.Clear();
playerJumpData.Clear();
jumpButtonHeld.Clear();
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null) return;
var data = new JumpData();
UpdatePermissionCache(player, data, "OnPlayerConnected");
playerJumpData[player.userID] = data;
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
playerJumpData.Remove(player.userID);
jumpButtonHeld.Remove(player.userID);
}
private void OnServerInitialized()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (!playerJumpData.ContainsKey(player.userID))
{
var data = new JumpData();
UpdatePermissionCache(player, data, "OnServerInitialized");
playerJumpData[player.userID] = data;
}
}
}
private object OnPlayerLand(BasePlayer player, float num)
{
if (player == null) return null;
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
if (data.FallDamageImmune)
{
float timeSinceDoubleJump = Time.realtimeSinceStartup - data.DoubleJumpTime;
float landingHeight = player.transform.position.y;
float fallDistance = data.DoubleJumpPeakHeight - landingHeight;
data.FallDamageImmune = false;
if (timeSinceDoubleJump <= FALL_DAMAGE_IMMUNITY_DURATION &&
fallDistance <= MAX_SAFE_FALL_HEIGHT &&
landingHeight >= data.DoubleJumpStartHeight - 3f)
{
player.fallVelocity = 0f;
return false;
}
}
}
return null;
}
private object OnPlayerInput(BasePlayer player, InputState input)
{
if (input == null || player == null) return null;
// Early filter
if (!input.IsDown(BUTTON.JUMP) && !input.WasDown(BUTTON.JUMP))
{
///Puts("return");
return null;
}
if (player.IsDead() || player.IsWounded() || player.isMounted || player.IsSleeping()) return null;
ulong playerId = player.userID;
bool isJumpPressed = input.IsDown(BUTTON.JUMP);
if (!isJumpPressed)
{
jumpButtonHeld.Remove(playerId);
///Puts("Remove");
return null;
}
if (jumpButtonHeld.Contains(playerId))
{
return null;
}
jumpButtonHeld.Add(playerId);
if (!playerJumpData.TryGetValue(playerId, out JumpData data))
{
data = new JumpData();
UpdatePermissionCache(player, data, "OnPlayerInput (new data)");
playerJumpData[playerId] = data;
}
if (!data.HasPermission) return null;
if (data.IsJumping) return null;
if (IsGrounded(player))
{
data.JumpTime = Time.realtimeSinceStartup;
data.CanDoubleJump = true;
data.FallDamageImmune = false;
}
else
{
if (data.CanDoubleJump)
{
float currentTime = Time.realtimeSinceStartup;
float timeSinceJump = currentTime - data.JumpTime;
float timeSinceLastDouble = currentTime - data.LastDoubleJumpTime;
if (timeSinceJump <= DOUBLE_JUMP_WINDOW && timeSinceLastDouble > JUMP_COOLDOWN)
{
if (HasCeilingAbove(player))
{
return null;
}
if (player.metabolism.calories.value >= config.DoubleJumpCalorieCost)
{
player.metabolism.calories.Subtract(config.DoubleJumpCalorieCost);
data.PreJumpPosition = player.transform.position;
data.HasValidPreJumpPosition = true;
PerformDoubleJump(player, data, data.JumpForce);
data.CanDoubleJump = false;
data.LastDoubleJumpTime = currentTime;
}
}
}
}
return null;
}
#endregion
#region Permission Hooks
private void OnUserPermissionGranted(string playerId, string perm)
{
if (!pluginPermissions.Contains(perm)) return;
var player = BasePlayer.Find(playerId);
if (player == null) return;
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
UpdatePermissionCache(player, data, $"OnUserPermissionGranted ({perm})");
}
}
private void OnUserPermissionRevoked(string playerId, string perm)
{
if (!pluginPermissions.Contains(perm)) return;
var player = BasePlayer.Find(playerId);
if (player == null) return;
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
UpdatePermissionCache(player, data, $"OnUserPermissionRevoked ({perm})");
}
}
private void OnGroupPermissionGranted(string group, string perm)
{
if (!pluginPermissions.Contains(perm)) return;
if (DEBUG_MODE)
Puts($"[Permission] Group '{group}' granted permission '{perm}', refreshing affected players");
RefreshAllPlayersInGroup(group);
}
private void OnGroupPermissionRevoked(string group, string perm)
{
if (!pluginPermissions.Contains(perm)) return;
if (DEBUG_MODE)
Puts($"[Permission] Group '{group}' revoked permission '{perm}', refreshing affected players");
RefreshAllPlayersInGroup(group);
}
private void OnUserGroupAdded(string playerId, string group)
{
var player = BasePlayer.Find(playerId);
if (player == null) return;
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
UpdatePermissionCache(player, data, $"OnUserGroupAdded ({group})");
}
}
private void OnUserGroupRemoved(string playerId, string group)
{
var player = BasePlayer.Find(playerId);
if (player == null) return;
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
UpdatePermissionCache(player, data, $"OnUserGroupRemoved ({group})");
}
}
#endregion
#region Chat Commands
[ChatCommand("posreset")]
private void CmdPosReset(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!playerJumpData.TryGetValue(player.userID, out JumpData data))
{
player.ChatMessage(Lang("NoData", player.UserIDString));
return;
}
if (data.IsJumping)
{
player.ChatMessage(Lang("JumpingCannotUse", player.UserIDString));
return;
}
if (!data.HasValidPreJumpPosition)
{
player.ChatMessage(Lang("NoValidPosition", player.UserIDString));
return;
}
Vector3 targetPos = data.PreJumpPosition;
if (Physics.Raycast(targetPos + Vector3.up * 2f, Vector3.down, out RaycastHit hit, 5f, PhysicsLayerMask))
{
targetPos = hit.point + Vector3.up * GROUND_OFFSET;
}
player.Teleport(targetPos);
player.SendNetworkUpdateImmediate();
player.ChatMessage(Lang("PositionReset", player.UserIDString));
}
#endregion
#region Helper Methods
private void UpdatePermissionCache(BasePlayer player, JumpData data, string source)
{
string id = player.UserIDString;
string grantedPerm = null;
data.HasPermission = false;
data.JumpForce = config.JumpForceUse;
if (permission.UserHasPermission(id, PERM_VIP3))
{
data.HasPermission = true;
data.JumpForce = config.JumpForceVip3;
grantedPerm = PERM_VIP3;
}
else if (permission.UserHasPermission(id, PERM_VIP2))
{
data.HasPermission = true;
data.JumpForce = config.JumpForceVip2;
grantedPerm = PERM_VIP2;
}
else if (permission.UserHasPermission(id, PERM_VIP1))
{
data.HasPermission = true;
data.JumpForce = config.JumpForceVip1;
grantedPerm = PERM_VIP1;
}
else if (permission.UserHasPermission(id, PERM_USE))
{
data.HasPermission = true;
data.JumpForce = config.JumpForceUse;
grantedPerm = PERM_USE;
}
if (DEBUG_MODE)
{
if (data.HasPermission)
Puts($"[Permission] {player.displayName} ({id}) - Source: {source} - HasPermission: true, Permission: {grantedPerm}, JumpForce: {data.JumpForce}");
else
Puts($"[Permission] {player.displayName} ({id}) - Source: {source} - HasPermission: false");
}
}
private void RefreshAllPlayersInGroup(string group)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (permission.UserHasGroup(player.UserIDString, group))
{
if (playerJumpData.TryGetValue(player.userID, out JumpData data))
{
UpdatePermissionCache(player, data, $"RefreshAllPlayersInGroup ({group})");
}
}
}
}
private bool IsGrounded(BasePlayer player)
{
if (player == null) return false;
Vector3 position = player.transform.position + Vector3.up * 0.1f;
if (Physics.Raycast(position, Vector3.down, GROUND_CHECK_DISTANCE + 0.1f, PhysicsLayerMask))
{
return true;
}
return player.IsOnGround();
}
private bool HasCeilingAbove(BasePlayer player)
{
if (player == null) return false;
Vector3 position = player.transform.position + Vector3.up * 1.8f;
if (Physics.Raycast(position, Vector3.up, out RaycastHit hit, CEILING_CHECK_DISTANCE, PhysicsLayerMask))
{
return true;
}
return false;
}
private void PerformDoubleJump(BasePlayer player, JumpData data, float jumpForce)
{
if (player == null) return;
data.DoubleJumpStartHeight = player.transform.position.y;
data.DoubleJumpPeakHeight = player.transform.position.y;
data.DoubleJumpTime = Time.realtimeSinceStartup;
data.FallDamageImmune = true;
data.IsJumping = true;
player.fallVelocity = 0f;
JumpBooster.Create(this, player, data, jumpForce);
}
private void RegisterJumpBooster(JumpBooster booster)
{
if (booster != null)
{
activeJumpBoosters.Add(booster);
}
}
private void UnregisterJumpBooster(JumpBooster booster)
{
if (booster != null)
{
activeJumpBoosters.Remove(booster);
}
}
#endregion
#region JumpBooster Class
private class JumpBooster : FacepunchBehaviour
{
private HighJump plugin;
private BasePlayer player;
private JumpData jumpData;
private BaseEntity boosterEntity;
private Rigidbody rb;
private float currentVelocity;
public static void Create(HighJump plugin, BasePlayer player, JumpData data, float force)
{
Vector3 spawnPosition = player.transform.position + Vector3.up * BOOSTER_SPAWN_HEIGHT;
var entity = GameManager.server.CreateEntity(
"assets/prefabs/misc/item drop/item_drop.prefab",
spawnPosition,
Quaternion.identity
) as DroppedItemContainer;
if (entity == null)
{
data.IsJumping = false;
return;
}
entity.enableSaving = false;
entity.Spawn();
Rigidbody rb = entity.GetComponent<Rigidbody>();
if (rb == null)
{
entity.Kill();
data.IsJumping = false;
return;
}
rb.isKinematic = true;
rb.useGravity = false;
player.SetParent(entity, true, true);
JumpBooster booster = entity.gameObject.AddComponent<JumpBooster>();
booster.plugin = plugin;
booster.player = player;
booster.jumpData = data;
booster.boosterEntity = entity;
booster.rb = rb;
booster.currentVelocity = force;
plugin.RegisterJumpBooster(booster);
}
private void FixedUpdate()
{
if (boosterEntity == null || boosterEntity.IsDestroyed || player == null || player.IsDead())
{
Cleanup();
return;
}
float deltaTime = Time.fixedDeltaTime;
currentVelocity -= GRAVITY * deltaTime;
if (player.transform.position.y > jumpData.DoubleJumpPeakHeight)
{
jumpData.DoubleJumpPeakHeight = player.transform.position.y;
}
if (currentVelocity > 0)
{
Vector3 playerPos = player.transform.position;
float moveDistance = currentVelocity * deltaTime;
Vector3 rayOrigin = playerPos + Vector3.up * PLAYER_HEAD_OFFSET;
if (Physics.SphereCast(rayOrigin, CEILING_CHECK_RADIUS, Vector3.up, out RaycastHit ceilingHit, moveDistance + CEILING_CHECK_MARGIN, PhysicsLayerMask))
{
float safeDistance = Mathf.Max(0f, ceilingHit.distance - CEILING_SAFE_MARGIN);
if (safeDistance > 0.01f)
{
boosterEntity.transform.position += Vector3.up * safeDistance;
}
currentVelocity = 0f;
CleanupSafe();
return;
}
boosterEntity.transform.position += Vector3.up * moveDistance;
}
else
{
Cleanup();
}
}
private void CleanupSafe()
{
enabled = false;
plugin?.UnregisterJumpBooster(this);
if (player != null && !player.IsDead())
{
Vector3 playerPos = player.transform.position;
if (playerPos.y > jumpData.DoubleJumpPeakHeight)
{
jumpData.DoubleJumpPeakHeight = playerPos.y;
}
player.SetParent(null, true, true);
Vector3 safePos = playerPos;
if (Physics.CheckSphere(playerPos + Vector3.up * STUCK_CHECK_HEIGHT, STUCK_CHECK_RADIUS, PhysicsLayerMask))
{
if (Physics.Raycast(playerPos + Vector3.up * SAFE_POS_RAY_START, Vector3.down, out RaycastHit groundHit, SAFE_POS_RAY_DISTANCE, PhysicsLayerMask))
{
safePos = groundHit.point + Vector3.up * GROUND_OFFSET;
}
}
player.Teleport(safePos);
player.fallVelocity = 0f;
player.SendNetworkUpdateImmediate();
}
if (jumpData != null)
{
jumpData.IsJumping = false;
}
KillEntity();
}
public void Cleanup()
{
enabled = false;
plugin?.UnregisterJumpBooster(this);
if (player != null && !player.IsDead())
{
if (player.transform.position.y > jumpData.DoubleJumpPeakHeight)
{
jumpData.DoubleJumpPeakHeight = player.transform.position.y;
}
player.SetParent(null, true, true);
player.fallVelocity = 0f;
player.SendNetworkUpdateImmediate();
}
if (jumpData != null)
{
jumpData.IsJumping = false;
}
KillEntity();
}
private void KillEntity()
{
if (boosterEntity != null && !boosterEntity.IsDestroyed)
{
var entityToKill = boosterEntity;
boosterEntity = null;
plugin?.NextTick(() =>
{
if (entityToKill != null && !entityToKill.IsDestroyed)
{
entityToKill.Kill();
}
});
}
Destroy(this);
}
private void OnDestroy()
{
plugin?.UnregisterJumpBooster(this);
if (jumpData != null)
{
jumpData.IsJumping = false;
}
}
}
#endregion
}
}