diff --git a/EXILED/Exiled.Events/EventArgs/Player/RoomChangedEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/RoomChangedEventArgs.cs
new file mode 100644
index 0000000000..57f17a3eda
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Player/RoomChangedEventArgs.cs
@@ -0,0 +1,44 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+namespace Exiled.Events.EventArgs.Player
+{
+ using Exiled.API.Features;
+ using Exiled.Events.EventArgs.Interfaces;
+ using MapGeneration;
+
+ ///
+ /// Contains the information when a player changes rooms.
+ ///
+ public class RoomChangedEventArgs : IPlayerEvent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player whose room has changed.
+ /// The room identifier before the change (Can be null on round start).
+ /// The room identifier after the change.
+ public RoomChangedEventArgs(ReferenceHub player, RoomIdentifier oldRoom, RoomIdentifier newRoom)
+ {
+ Player = Player.Get(player);
+ OldRoom = Room.Get(oldRoom);
+ NewRoom = Room.Get(newRoom);
+ }
+
+ ///
+ public Player Player { get; }
+
+ ///
+ /// Gets the previous room the player was in.
+ ///
+ public Room OldRoom { get; }
+
+ ///
+ /// Gets the new room the player entered.
+ ///
+ public Room NewRoom { get; }
+ }
+}
diff --git a/EXILED/Exiled.Events/EventArgs/Player/ZoneChangedEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ZoneChangedEventArgs.cs
new file mode 100644
index 0000000000..7d8c0c714d
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Player/ZoneChangedEventArgs.cs
@@ -0,0 +1,58 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.EventArgs.Player
+{
+ using Exiled.API.Enums;
+ using Exiled.API.Features;
+ using Exiled.Events.EventArgs.Interfaces;
+ using MapGeneration;
+
+ ///
+ /// Contains the information when a player changes zones.
+ ///
+ public class ZoneChangedEventArgs : IPlayerEvent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player whose zone has changed.
+ /// The previous room the player was in.
+ /// The new room the player entered.
+ public ZoneChangedEventArgs(ReferenceHub player, RoomIdentifier oldRoom, RoomIdentifier newRoom)
+ {
+ Player = Player.Get(player);
+ OldRoom = Room.Get(oldRoom);
+ NewRoom = Room.Get(newRoom);
+ OldZone = OldRoom.Zone;
+ NewZone = NewRoom.Zone;
+ }
+
+ ///
+ public Player Player { get; }
+
+ ///
+ /// Gets the previous zone the player was in.
+ ///
+ public ZoneType OldZone { get; }
+
+ ///
+ /// Gets the new zone the player entered.
+ ///
+ public ZoneType NewZone { get; }
+
+ ///
+ /// Gets the previous room the player was in.
+ ///
+ public Room OldRoom { get; }
+
+ ///
+ /// Gets the new room the player entered.
+ ///
+ public Room NewRoom { get; }
+ }
+}
diff --git a/EXILED/Exiled.Events/Handlers/Player.cs b/EXILED/Exiled.Events/Handlers/Player.cs
index e35df7b044..b0657a2d62 100644
--- a/EXILED/Exiled.Events/Handlers/Player.cs
+++ b/EXILED/Exiled.Events/Handlers/Player.cs
@@ -489,6 +489,16 @@ public class Player
///
public static Event ChangingSpectatedPlayer { get; set; } = new();
+ ///
+ /// Invoked when a changes rooms.
+ ///
+ public static Event RoomChanged { get; set; } = new();
+
+ ///
+ /// Invoked when a changes zones.
+ ///
+ public static Event ZoneChanged { get; set; } = new();
+
///
/// Invoked before a toggles the NoClip mode.
///
@@ -799,6 +809,18 @@ public class Player
/// The instance.
public static void OnRemovedHandcuffs(RemovedHandcuffsEventArgs ev) => RemovedHandcuffs.InvokeSafely(ev);
+ ///
+ /// Called when a changes rooms.
+ ///
+ /// The instance.
+ public static void OnRoomChanged(RoomChangedEventArgs ev) => RoomChanged.InvokeSafely(ev);
+
+ ///
+ /// Called when a changes zones.
+ ///
+ /// The instance.
+ public static void OnZoneChanged(ZoneChangedEventArgs ev) => ZoneChanged.InvokeSafely(ev);
+
///
/// Called before a escapes.
///
diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ChangedRoomZone.cs b/EXILED/Exiled.Events/Patches/Events/Player/ChangedRoomZone.cs
new file mode 100644
index 0000000000..545f52cef7
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Events/Player/ChangedRoomZone.cs
@@ -0,0 +1,117 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Events.Player
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Features.Pools;
+ using Exiled.Events.Attributes;
+ using Exiled.Events.EventArgs.Player;
+ using Exiled.Events.Handlers;
+ using HarmonyLib;
+ using MapGeneration;
+ using UnityEngine;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches to add the and events.
+ ///
+ [EventPatch(typeof(Player), nameof(Player.RoomChanged))]
+ [EventPatch(typeof(Player), nameof(Player.ZoneChanged))]
+ [HarmonyPatch(typeof(CurrentRoomPlayerCache), nameof(CurrentRoomPlayerCache.ValidateCache))]
+ internal class ChangedRoomZone
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label returnLabel = generator.DefineLabel();
+
+ LocalBuilder oldRoom = generator.DeclareLocal(typeof(RoomIdentifier));
+ LocalBuilder newRoom = generator.DeclareLocal(typeof(RoomIdentifier));
+
+ int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldloca_S);
+
+ newInstructions.InsertRange(index, new CodeInstruction[]
+ {
+ // oldRoom = this._lastDetected
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(typeof(CurrentRoomPlayerCache), nameof(CurrentRoomPlayerCache._lastDetected))),
+ new(OpCodes.Stloc_S, oldRoom),
+ });
+
+ int lastIndex = newInstructions.Count - 1;
+
+ newInstructions[lastIndex].WithLabels(returnLabel);
+
+ newInstructions.InsertRange(lastIndex, new CodeInstruction[]
+ {
+ // newRoom = this._lastDetected
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(typeof(CurrentRoomPlayerCache), nameof(CurrentRoomPlayerCache._lastDetected))),
+ new(OpCodes.Dup),
+ new(OpCodes.Stloc_S, newRoom),
+
+ new(OpCodes.Ldloc_S, oldRoom),
+
+ // if (oldRoom == newRoom) return;
+ new(OpCodes.Call, Method(typeof(object), nameof(object.Equals), new[] { typeof(object), typeof(object) })),
+ new(OpCodes.Brtrue_S, returnLabel),
+
+ // ReferenceHub hub = this._roleManager.gameObject.GetComponent();
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(typeof(CurrentRoomPlayerCache), nameof(CurrentRoomPlayerCache._roleManager))),
+ new(OpCodes.Call, Method(typeof(Component), nameof(Component.GetComponent)).MakeGenericMethod(typeof(ReferenceHub))),
+
+ // oldRoom
+ new(OpCodes.Ldloc_S, oldRoom),
+
+ // newRoom
+ new(OpCodes.Ldloc_S, newRoom),
+
+ // Handlers.Player.OnRoomChanged(new RoomChangedEventArgs(hub, oldRoom, newRoom));
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(RoomChangedEventArgs))[0]),
+ new(OpCodes.Call, Method(typeof(Player), nameof(Player.OnRoomChanged))),
+
+ // oldRoom.Zone
+ new(OpCodes.Ldloc_S, oldRoom),
+ new(OpCodes.Ldfld, Field(typeof(RoomIdentifier), nameof(RoomIdentifier.Zone))),
+
+ // newRoom.Zone
+ new(OpCodes.Ldloc_S, newRoom),
+ new(OpCodes.Ldfld, Field(typeof(RoomIdentifier), nameof(RoomIdentifier.Zone))),
+
+ // if (oldRoom.Zone == newRoom.Zone) return;
+ new(OpCodes.Ceq),
+ new(OpCodes.Brtrue_S, returnLabel),
+
+ // ReferenceHub hub = this._roleManager.gameObject.GetComponent();
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(typeof(CurrentRoomPlayerCache), nameof(CurrentRoomPlayerCache._roleManager))),
+ new(OpCodes.Call, Method(typeof(Component), nameof(Component.GetComponent)).MakeGenericMethod(typeof(ReferenceHub))),
+
+ // oldRoom
+ new(OpCodes.Ldloc_S, oldRoom),
+
+ // newRoom
+ new(OpCodes.Ldloc_S, newRoom),
+
+ // Handlers.Player.OnZoneChanged(new ZoneChangedEventArgs(hub, oldRoom, newRoom));
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ZoneChangedEventArgs))[0]),
+ new(OpCodes.Call, Method(typeof(Player), nameof(Player.OnZoneChanged))),
+ });
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}