diff --git a/EXILED/Exiled.API/Enums/FirearmType.cs b/EXILED/Exiled.API/Enums/FirearmType.cs
index 05cccf2388..499bc40678 100644
--- a/EXILED/Exiled.API/Enums/FirearmType.cs
+++ b/EXILED/Exiled.API/Enums/FirearmType.cs
@@ -7,6 +7,8 @@
namespace Exiled.API.Enums
{
+ using System;
+
///
/// Represents a firearm.
///
@@ -18,6 +20,7 @@ namespace Exiled.API.Enums
///
///
///
+ [Flags]
public enum FirearmType
{
///
diff --git a/EXILED/Exiled.API/Extensions/IEnumerableExtensions.cs b/EXILED/Exiled.API/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 0000000000..a46ac482a6
--- /dev/null
+++ b/EXILED/Exiled.API/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,33 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Extensions
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// A set of extensions for .
+ ///
+ public static class IEnumerableExtensions
+ {
+ ///
+ /// Perform an action on each element of a collection.
+ ///
+ /// Type of elements.
+ /// in this collection, the elements will perform actions.
+ /// Action that needs to be performed.
+ public static void ForEach(this IEnumerable enumerable, Action action)
+ {
+ if (enumerable is null || action is null)
+ return;
+
+ foreach (T e in enumerable)
+ action(e);
+ }
+ }
+}
diff --git a/EXILED/Exiled.API/Extensions/PlayerPermissionsExtensions.cs b/EXILED/Exiled.API/Extensions/PlayerPermissionsExtensions.cs
new file mode 100644
index 0000000000..91db09c4e1
--- /dev/null
+++ b/EXILED/Exiled.API/Extensions/PlayerPermissionsExtensions.cs
@@ -0,0 +1,48 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Extensions
+{
+ using System.Collections.Generic;
+
+ ///
+ /// A set of extensions for .
+ ///
+ public static class PlayerPermissionsExtensions
+ {
+ ///
+ /// Checks whether the current permissions contain any of the permissions specified in the mask.
+ ///
+ /// The current permissions to check.
+ /// The mask of permissions to test against.
+ /// if the current permissions contain at least one permission from the mask; otherwise, .
+ public static bool HasAnyPermission(this PlayerPermissions playerPermissions, PlayerPermissions mask)
+ {
+ return (playerPermissions & mask) != 0;
+ }
+
+ ///
+ /// Checks whether the current permissions contain any of the permissions specified in the collection.
+ ///
+ /// The current permissions to check.
+ /// The collection of permissions to test against.
+ /// if the current permissions contain at least one permission from the collection; otherwise, .
+ public static bool HasAnyPermission(this PlayerPermissions playerPermissions, IEnumerable collectionPlayerPermissions)
+ {
+ if (collectionPlayerPermissions is null)
+ return false;
+
+ foreach (PlayerPermissions perm in collectionPlayerPermissions)
+ {
+ if (playerPermissions.HasAnyPermission(perm))
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/EXILED/Exiled.API/Extensions/RandomExtensions.cs b/EXILED/Exiled.API/Extensions/RandomExtensions.cs
new file mode 100644
index 0000000000..8e752b91f4
--- /dev/null
+++ b/EXILED/Exiled.API/Extensions/RandomExtensions.cs
@@ -0,0 +1,75 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Extensions
+{
+ using System;
+
+ ///
+ /// A set of extensions for .
+ ///
+ public static class RandomExtensions
+ {
+ ///
+ /// Generate a random float.
+ ///
+ /// object.
+ /// Minimum value.
+ /// Maximum value.
+ /// Random value between minimum and maximum.
+ public static float NextFloat(this Random rnd, float min, float max)
+ {
+ return (float)((rnd.NextDouble() * (max - min)) + min);
+ }
+
+ ///
+ /// Generate a random float.
+ ///
+ /// object.
+ /// Minimum value.
+ /// Maximum value.
+ /// Random value between minimum and maximum.
+ public static float NextFloat(this Random rnd, double min, float max)
+ {
+ return (float)((rnd.NextDouble() * (max - min)) + min);
+ }
+
+ ///
+ /// Generate a random float.
+ ///
+ /// object.
+ /// Minimum value.
+ /// Maximum value.
+ /// Random value between minimum and maximum.
+ public static float NextFloat(this Random rnd, float min, double max)
+ {
+ return (float)((rnd.NextDouble() * (max - min)) + min);
+ }
+
+ ///
+ /// Generate a random float.
+ ///
+ /// object.
+ /// Minimum value.
+ /// Maximum value.
+ /// Random value between minimum and maximum.
+ public static float NextFloat(this Random rnd, double min, double max)
+ {
+ return (float)((rnd.NextDouble() * (max - min)) + min);
+ }
+
+ ///
+ /// Generate a random bool.
+ ///
+ /// object.
+ /// Random boolean value.
+ public static bool NextBool(this Random rnd)
+ {
+ return (rnd.Next() & 1) == 0;
+ }
+ }
+}
diff --git a/EXILED/Exiled.API/Extensions/TimeSpanExtensions.cs b/EXILED/Exiled.API/Extensions/TimeSpanExtensions.cs
new file mode 100644
index 0000000000..b08718729e
--- /dev/null
+++ b/EXILED/Exiled.API/Extensions/TimeSpanExtensions.cs
@@ -0,0 +1,35 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Extensions
+{
+ using System;
+
+ ///
+ /// A set of extensions for .
+ ///
+ public static class TimeSpanExtensions
+ {
+ ///
+ /// Converts a TimeSpan object to a human-readable format.
+ ///
+ /// object.
+ /// A object in string representation.
+ public static string ToHumanReadable(this TimeSpan timeSpan)
+ {
+ if (timeSpan.TotalHours < 1)
+ return timeSpan.ToString(@"mm\:ss");
+
+ if (timeSpan.TotalDays < 1)
+ return timeSpan.ToString(@"hh\:mm\:ss");
+
+ string daysPart = timeSpan.Days == 1 ? "1 day" : $"{timeSpan.Days} days";
+ string timePart = timeSpan.ToString(@"hh\:mm\:ss");
+ return $"{daysPart}, {timePart}";
+ }
+ }
+}
diff --git a/EXILED/Exiled.API/Extensions/Vector3Extensions.cs b/EXILED/Exiled.API/Extensions/Vector3Extensions.cs
new file mode 100644
index 0000000000..5667db3fab
--- /dev/null
+++ b/EXILED/Exiled.API/Extensions/Vector3Extensions.cs
@@ -0,0 +1,54 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Extensions
+{
+ using Exiled.API.Enums;
+ using Exiled.API.Features;
+
+ using UnityEngine;
+
+ ///
+ /// A set of extensions for that provide conversions between world space and
+ /// room‑relative local space.
+ ///
+ public static class Vector3Extensions
+ {
+ ///
+ /// Converts a world position to a position relative to the specified room's local coordinate system.
+ ///
+ /// The world‑space position to convert.
+ /// The room whose local space will be used as the reference.
+ ///
+ /// The position expressed in the room's local space.
+ /// If the room is the , the original world position is returned unchanged.
+ ///
+ public static Vector3 FromWorldToRelativePos(this Vector3 worldPos, Room room)
+ {
+ if (room.Type == RoomType.Surface)
+ return worldPos;
+ return room.Transform.InverseTransformPoint(worldPos);
+ }
+
+ ///
+ /// Converts a position relative to the specified room's local space back to world space.
+ ///
+ /// The local‑space position to convert.
+ /// The room whose local space was used as the reference.
+ ///
+ /// The position expressed in world space.
+ /// If the room is the , the original local position is returned unchanged
+ /// (since surface uses world coordinates directly).
+ ///
+ public static Vector3 FromRelativeToWorldPos(this Vector3 relativePos, Room room)
+ {
+ if (room.Type == RoomType.Surface)
+ return relativePos;
+ return room.Transform.TransformPoint(relativePos);
+ }
+ }
+}
diff --git a/EXILED/Exiled.API/Features/Doors/Door.cs b/EXILED/Exiled.API/Features/Doors/Door.cs
index a60da6e36c..7c6016d635 100644
--- a/EXILED/Exiled.API/Features/Doors/Door.cs
+++ b/EXILED/Exiled.API/Features/Doors/Door.cs
@@ -337,6 +337,28 @@ public static Door Get(DoorVariant doorVariant)
public static T Get(DoorVariant doorVariant)
where T : Door => Get(doorVariant) as T;
+ ///
+ /// Gets the door object associated with a specific .
+ ///
+ /// The base-game .
+ /// A wrapper object.
+ public static Door Get(ButtonVariant buttonVariant)
+ {
+ if (buttonVariant is null || buttonVariant.ParentDoor is null)
+ return null;
+
+ return Get(buttonVariant.ParentDoor);
+ }
+
+ ///
+ /// Gets the by .
+ ///
+ /// The to convert into an door.
+ /// The specified type.
+ /// The door wrapper for the given .
+ public static T Get(ButtonVariant buttonVariant)
+ where T : Door => Get(buttonVariant) as T;
+
///
/// Gets a given the specified .
///
@@ -378,7 +400,16 @@ public static T Get(string name)
///
/// The base-game .
/// The with the given name or if not found.
- public static Door Get(GameObject gameObject) => gameObject is null ? null : Get(gameObject.GetComponentInParent());
+ public static Door Get(GameObject gameObject)
+ {
+ if (gameObject != null)
+ {
+ // ParentDoor requires enabling "unsafe code"
+ return Get(gameObject.GetComponentInParent() ?? gameObject.GetComponent()?.ParentDoor);
+ }
+
+ return null;
+ }
///
/// Returns the closest to the given .
diff --git a/EXILED/Exiled.API/Features/Items/Firearm.cs b/EXILED/Exiled.API/Features/Items/Firearm.cs
index 9cd3a13395..467e307d4f 100644
--- a/EXILED/Exiled.API/Features/Items/Firearm.cs
+++ b/EXILED/Exiled.API/Features/Items/Firearm.cs
@@ -143,7 +143,7 @@ public static IReadOnlyDictionary
- /// Gets a primaty magazine for current firearm.
+ /// Gets a primary magazine for current firearm.
///
public PrimaryMagazine PrimaryMagazine { get; }
@@ -156,7 +156,7 @@ public static IReadOnlyDictionary
- /// Gets a primaty magazine for current firearm.
+ /// Gets a primary magazine for current firearm.
///
public HitscanHitregModuleBase HitscanHitregModule { get; }
@@ -189,6 +189,36 @@ public int MagazineAmmo
set => PrimaryMagazine.Ammo = value;
}
+ ///
+ /// Gets or sets a value indicating whether the magazine is attached from the weapon. Setter will attach the magazine, but it will be empty. Weapons that do not have a detachable magazine return false by default. For example, a revolver.
+ ///
+ public bool IsMagazineAttached
+ {
+ get
+ {
+ if (PrimaryMagazine is NormalMagazine normalMag)
+ return normalMag.MagazineInserted;
+
+ // Weapons that do not have a detachable magazine return false by default. For example, a revolver.
+ return false;
+ }
+
+ set
+ {
+ if (PrimaryMagazine is NormalMagazine normalMag)
+ normalMag.MagazineInserted = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the magazine is attached from the weapon.
+ ///
+ public bool IsMagazineDeattached
+ {
+ get => !IsMagazineAttached;
+ set => IsMagazineAttached = !value;
+ }
+
///
/// Gets or sets the amount of ammo in the firearm barrel.
///
diff --git a/EXILED/Exiled.API/Features/Log.cs b/EXILED/Exiled.API/Features/Log.cs
index 48203577c6..9229ba8681 100644
--- a/EXILED/Exiled.API/Features/Log.cs
+++ b/EXILED/Exiled.API/Features/Log.cs
@@ -73,7 +73,7 @@ public static T DebugObject(T @object)
/// Server must have exiled_debug config enabled.
///
/// The message to be sent.
- public static void Debug(string message)
+ public static void Debug(string message = "")
{
Assembly callingAssembly = Assembly.GetCallingAssembly();
#if DEBUG
@@ -98,7 +98,7 @@ public static void Debug(string message)
/// Sends a level messages to the game console.
///
/// The message to be sent.
- public static void Warn(string message) => Send($"[{Assembly.GetCallingAssembly().GetName().Name}] {message}", Discord.LogLevel.Warn, ConsoleColor.Magenta);
+ public static void Warn(string message = "") => Send($"[{Assembly.GetCallingAssembly().GetName().Name}] {message}", Discord.LogLevel.Warn, ConsoleColor.Magenta);
///
/// Sends a level messages to the game console.
@@ -114,7 +114,7 @@ public static void Debug(string message)
/// It's recommended to send any messages in the catch block of a try/catch as errors with the exception string.
///
/// The message to be sent.
- public static void Error(string message) => Send($"[{Assembly.GetCallingAssembly().GetName().Name}] {message}", Discord.LogLevel.Error, ConsoleColor.DarkRed);
+ public static void Error(string message = "") => Send($"[{Assembly.GetCallingAssembly().GetName().Name}] {message}", Discord.LogLevel.Error, ConsoleColor.DarkRed);
///
/// Sends a log message to the game console.
diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs
index 6a46c50049..4cced1556b 100644
--- a/EXILED/Exiled.API/Features/Player.cs
+++ b/EXILED/Exiled.API/Features/Player.cs
@@ -53,7 +53,6 @@ namespace Exiled.API.Features
using Mirror.LiteNetLib4Mirror;
using PlayerRoles;
using PlayerRoles.FirstPersonControl;
- using PlayerRoles.FirstPersonControl.Thirdperson;
using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers;
using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers.Wearables;
using PlayerRoles.RoleAssign;
@@ -4008,6 +4007,39 @@ public void Teleport(object obj, Vector3 offset)
}
}
+ ///
+ /// Teleports the player to the specified world position on the next frame. This method is useful when you need to delay the teleport until the next update cycle, for example, after modifying the player's state or to avoid conflicts with other operations.
+ ///
+ /// The world position to teleport the player to.
+ public void TeleportNextFrame(Vector3 position)
+ {
+ Timing.CallDelayed(Timing.WaitForOneFrame, () =>
+ {
+ if (!IsConnected)
+ return;
+
+ Position = position;
+ });
+ }
+
+ ///
+ /// Teleports the player to a position obtained from a delegate on the next frame. The delegate is invoked on the next frame, allowing you to compute the position dynamically based on the current game state.
+ ///
+ /// A function that returns the world position to teleport to. If this delegate is , the method does nothing.
+ public void TeleportNextFrame(Func getPosition)
+ {
+ if (getPosition is null)
+ return;
+
+ Timing.CallDelayed(Timing.WaitForOneFrame, () =>
+ {
+ if (!IsConnected)
+ return;
+
+ Position = getPosition.Invoke();
+ });
+ }
+
///
/// Teleports player to a random object of a specific type.
///
@@ -4233,8 +4265,7 @@ public void SetCooldownItem(float time, ItemType itemType)
///
public override bool Equals(object obj)
{
- Player player = obj as Player;
- return (object)player != null && ReferenceHub == player.ReferenceHub;
+ return obj is Player player && ReferenceHub == player.ReferenceHub;
}
///
diff --git a/EXILED/Exiled.API/Features/Room.cs b/EXILED/Exiled.API/Features/Room.cs
index 45a86e7580..c0f908a0d4 100644
--- a/EXILED/Exiled.API/Features/Room.cs
+++ b/EXILED/Exiled.API/Features/Room.cs
@@ -500,7 +500,7 @@ private static RoomType FindType(GameObject gameObject)
"HCZ_Corner_Deep" => RoomType.HczCornerDeep,
"HCZ_Straight" => RoomType.HczStraight,
"HCZ_Straight_C" => RoomType.HczStraightC,
- "HCZ_Straight_PipeRoom"=> RoomType.HczStraightPipeRoom,
+ "HCZ_Straight_PipeRoom" => RoomType.HczStraightPipeRoom,
"HCZ_Straight Variant" => RoomType.HczStraightVariant,
"HCZ_ChkpA" => RoomType.HczElevatorA,
"HCZ_ChkpB" => RoomType.HczElevatorB,
diff --git a/EXILED/Exiled.API/Features/Server.cs b/EXILED/Exiled.API/Features/Server.cs
index 825cfb3566..9e32b28cb4 100644
--- a/EXILED/Exiled.API/Features/Server.cs
+++ b/EXILED/Exiled.API/Features/Server.cs
@@ -11,8 +11,6 @@ namespace Exiled.API.Features
using System.Collections.Generic;
using System.Reflection;
- using Exiled.API.Enums;
-
using GameCore;
using Interfaces;
@@ -178,6 +176,15 @@ public static int MaxPlayerCount
set => CustomNetworkManager.slots = value;
}
+ ///
+ /// Gets or sets the number of spare slots.
+ ///
+ public static int ReservedSlots
+ {
+ get => CustomNetworkManager.reservedSlots;
+ set => CustomNetworkManager.reservedSlots = value;
+ }
+
///
/// Gets a value indicating whether late join is enabled.
///
diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs b/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs
index 789fd25ceb..728b532ffb 100644
--- a/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs
+++ b/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs
@@ -242,7 +242,7 @@ public static bool TryGet(Item item, out CustomItem? customItem)
/// True if the pickup is a custom item.
public static bool TryGet(Pickup pickup, out CustomItem? customItem)
{
- customItem = Registered?.FirstOrDefault(tempCustomItem => tempCustomItem.TrackedSerials.Contains(pickup.Serial));
+ customItem = pickup == null ? null : Registered?.FirstOrDefault(tempCustomItem => tempCustomItem.TrackedSerials.Contains(pickup.Serial));
return customItem is not null;
}